From c866142770eedd6acc36892fcf3c15b45a54062a Mon Sep 17 00:00:00 2001 From: Ryth Azhur Date: Tue, 31 Mar 2026 20:01:03 -0400 Subject: [PATCH] Rename djarea to mizan and fix React casing conventions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the package from djarea to mizan across the entire codebase — Python package, React library, generators, tests, and examples. Fix JSX/hook casing (MizanProvider, useMizan, etc.) that broke when the original PascalCase names were lowercased during the rename. Co-Authored-By: Claude Opus 4.6 (1M context) --- Dockerfile.test | 2 +- MIZAN.md | 2 +- Makefile | 4 +- README.md | 335 +++++++----------- desktop/app.py | 8 +- desktop/backend/asgi.py | 4 +- desktop/backend/djarea_clients.py | 42 ++- desktop/backend/settings.py | 2 +- desktop/backend/urls.py | 2 +- desktop/frontend/index.html | 2 +- desktop/frontend/package.json | 4 +- desktop/frontend/src/App.tsx | 16 +- desktop/pyproject.toml | 8 +- desktop/tests/conftest.py | 5 +- desktop/tests/test_desktop_rpc.py | 24 +- desktop/tests/test_notes.py | 5 +- desktop/tests/test_system.py | 21 +- django/README.md | 24 +- django/pyproject.toml | 6 +- django/src/djarea/shapes/__init__.py | 3 - django/src/{djarea => mizan}/__init__.py | 37 +- .../src/{djarea => mizan}/_vendor/__init__.py | 0 .../{djarea => mizan}/_vendor/app_visitor.py | 0 .../{djarea => mizan}/channels/__init__.py | 84 +++-- .../{djarea => mizan}/channels/connection.py | 270 ++++++++------ django/src/{djarea => mizan}/channels/push.py | 13 +- .../src/{djarea => mizan}/client/__init__.py | 4 +- .../src/{djarea => mizan}/client/executor.py | 33 +- .../src/{djarea => mizan}/client/function.py | 78 ++-- django/src/{djarea => mizan}/client/jwt.py | 10 +- .../src/{djarea => mizan}/export/__init__.py | 39 +- .../src/{djarea => mizan}/forms/__init__.py | 101 +++--- .../{djarea => mizan}/forms/formset_utils.py | 0 .../{djarea => mizan}/forms/schema_utils.py | 0 django/src/{djarea => mizan}/forms/schemas.py | 0 .../forms/validation_utils.py | 0 .../integrations/allauth/__init__.py | 8 +- .../integrations/allauth/contexts.py | 21 +- .../integrations/allauth/forms.py | 86 +++-- django/src/{djarea => mizan}/jwt/__init__.py | 11 +- django/src/{djarea => mizan}/jwt/functions.py | 18 +- django/src/{djarea => mizan}/jwt/security.py | 0 django/src/{djarea => mizan}/jwt/settings.py | 0 django/src/{djarea => mizan}/jwt/tokens.py | 0 .../{djarea => mizan}/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/export_channels_schema.py | 2 +- .../commands/export_djarea_schema.py | 16 +- .../src/{djarea => mizan}/setup/__init__.py | 18 +- .../src/{djarea => mizan}/setup/discovery.py | 32 +- .../src/{djarea => mizan}/setup/registry.py | 30 +- .../src/{djarea => mizan}/setup/settings.py | 16 +- django/src/mizan/shapes/__init__.py | 3 + django/src/{djarea => mizan}/shapes/core.py | 0 .../src/{djarea => mizan}/tests/__init__.py | 0 .../src/{djarea => mizan}/tests/test_auth.py | 59 ++- .../tests/test_benchmarks.py | 65 ++-- .../{djarea => mizan}/tests/test_channels.py | 170 +++++---- .../src/{djarea => mizan}/tests/test_core.py | 158 ++++++--- .../{djarea => mizan}/tests/test_pentest.py | 95 +++-- .../{djarea => mizan}/tests/test_security.py | 165 +++++---- .../{djarea => mizan}/tests/test_shapes.py | 200 ++++++++--- django/src/{djarea => mizan}/urls.py | 8 +- django/tests/models.py | 18 +- django/tests/settings.py | 4 +- django/tests/urls.py | 2 +- e2e/djarea.spec.ts | 6 +- e2e/harness/index.html | 2 +- e2e/harness/package.json | 4 +- e2e/harness/src/api/index.ts | 12 +- e2e/harness/src/fixtures.tsx | 8 +- e2e/harness/src/main.tsx | 2 +- e2e/harness/vite.config.ts | 22 +- example/testapp/apps.py | 2 +- example/testapp/asgi.py | 4 +- example/testapp/djarea_clients.py | 66 ++-- example/testapp/settings.py | 2 +- example/testapp/urls.py | 2 +- package.json | 2 +- react/README.md | 22 +- react/package.json | 4 +- react/src/__tests__/context.test.tsx | 50 +-- react/src/__tests__/forms.test.tsx | 2 +- react/src/__tests__/integration.test.tsx | 20 +- react/src/allauth/adapters/router.ts | 4 +- .../src/allauth/components/AuthDjangoForm.tsx | 4 +- .../components/settings/EmailsSection.tsx | 2 +- .../components/settings/PasswordSection.tsx | 2 +- react/src/allauth/config.ts | 2 +- react/src/allauth/contexts/APIContext.tsx | 2 +- react/src/allauth/contexts/AllauthContext.tsx | 2 +- react/src/allauth/contexts/AuthContext.tsx | 14 +- react/src/allauth/hydration.ts | 2 +- react/src/allauth/index.ts | 8 +- react/src/allauth/nextjs.tsx | 8 +- react/src/channels/connection.ts | 2 +- react/src/channels/context.tsx | 2 +- react/src/channels/hooks.ts | 2 +- react/src/channels/index.ts | 6 +- react/src/channels/types.ts | 2 +- react/src/client/index.ts | 16 +- react/src/client/nextjs.tsx | 4 +- react/src/client/react.ts | 6 +- react/src/client/types.ts | 2 +- react/src/context.tsx | 102 +++--- react/src/forms.ts | 18 +- react/src/generator/cli.mjs | 116 +++--- react/src/generator/lib/channels.mjs | 12 +- react/src/generator/lib/djarea.mjs | 108 +++--- react/src/generator/lib/fetch.mjs | 10 +- react/src/generator/lib/index.mjs | 34 +- react/src/index.ts | 34 +- react/src/jwt/JWTContext.tsx | 2 +- react/src/jwt/__tests__/contract.test.ts | 18 +- react/src/jwt/index.ts | 16 +- react/src/testing.ts | 2 +- react/tsconfig.build.json | 4 +- react/vitest.config.ts | 18 +- 118 files changed, 1778 insertions(+), 1433 deletions(-) delete mode 100644 django/src/djarea/shapes/__init__.py rename django/src/{djarea => mizan}/__init__.py (84%) rename django/src/{djarea => mizan}/_vendor/__init__.py (100%) rename django/src/{djarea => mizan}/_vendor/app_visitor.py (100%) rename django/src/{djarea => mizan}/channels/__init__.py (89%) rename django/src/{djarea => mizan}/channels/connection.py (71%) rename django/src/{djarea => mizan}/channels/push.py (96%) rename django/src/{djarea => mizan}/client/__init__.py (90%) rename django/src/{djarea => mizan}/client/executor.py (94%) rename django/src/{djarea => mizan}/client/function.py (91%) rename django/src/{djarea => mizan}/client/jwt.py (72%) rename django/src/{djarea => mizan}/export/__init__.py (91%) rename django/src/{djarea => mizan}/forms/__init__.py (88%) rename django/src/{djarea => mizan}/forms/formset_utils.py (100%) rename django/src/{djarea => mizan}/forms/schema_utils.py (100%) rename django/src/{djarea => mizan}/forms/schemas.py (100%) rename django/src/{djarea => mizan}/forms/validation_utils.py (100%) rename django/src/{djarea => mizan}/integrations/allauth/__init__.py (64%) rename django/src/{djarea => mizan}/integrations/allauth/contexts.py (87%) rename django/src/{djarea => mizan}/integrations/allauth/forms.py (84%) rename django/src/{djarea => mizan}/jwt/__init__.py (83%) rename django/src/{djarea => mizan}/jwt/functions.py (85%) rename django/src/{djarea => mizan}/jwt/security.py (100%) rename django/src/{djarea => mizan}/jwt/settings.py (100%) rename django/src/{djarea => mizan}/jwt/tokens.py (100%) rename django/src/{djarea => mizan}/management/__init__.py (100%) rename django/src/{djarea => mizan}/management/commands/__init__.py (100%) rename django/src/{djarea => mizan}/management/commands/export_channels_schema.py (93%) rename django/src/{djarea => mizan}/management/commands/export_djarea_schema.py (68%) rename django/src/{djarea => mizan}/setup/__init__.py (80%) rename django/src/{djarea => mizan}/setup/discovery.py (68%) rename django/src/{djarea => mizan}/setup/registry.py (91%) rename django/src/{djarea => mizan}/setup/settings.py (55%) create mode 100644 django/src/mizan/shapes/__init__.py rename django/src/{djarea => mizan}/shapes/core.py (100%) rename django/src/{djarea => mizan}/tests/__init__.py (100%) rename django/src/{djarea => mizan}/tests/test_auth.py (95%) rename django/src/{djarea => mizan}/tests/test_benchmarks.py (92%) rename django/src/{djarea => mizan}/tests/test_channels.py (90%) rename django/src/{djarea => mizan}/tests/test_core.py (91%) rename django/src/{djarea => mizan}/tests/test_pentest.py (95%) rename django/src/{djarea => mizan}/tests/test_security.py (91%) rename django/src/{djarea => mizan}/tests/test_shapes.py (79%) rename django/src/{djarea => mizan}/urls.py (84%) diff --git a/Dockerfile.test b/Dockerfile.test index d976219..9c04acc 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -7,7 +7,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ gcc \ && rm -rf /var/lib/apt/lists/* -# Install djarea from local source with channels support +# Install mizan from local source with channels support COPY django/ /app/django/ RUN pip install --no-cache-dir /app/django[channels] daphne diff --git a/MIZAN.md b/MIZAN.md index e467de0..f3debd4 100644 --- a/MIZAN.md +++ b/MIZAN.md @@ -6,7 +6,7 @@ This plan was written by Ryth's Claude.ai session after an extended design conve reviewing the full codebase, the original @compose discussion from January 2025, and several rounds of architectural refinement. Treat this as the spec. -The framework formerly called Djarea is now called **MIZAN**. Package names, imports, +The framework formerly called mizan is now called **MIZAN**. Package names, imports, and references should be updated accordingly. The internal codegen engine is called **Maison** — it lives inside Mizan and does not need its own public surface. diff --git a/Makefile b/Makefile index 15e5922..fae896d 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ test-react: test-integration: docker-up @echo "Waiting for backend..." - @timeout 30 sh -c 'until curl -sf http://localhost:8000/api/djarea/session/ > /dev/null 2>&1; do sleep 1; done' + @timeout 30 sh -c 'until curl -sf http://localhost:8000/api/mizan/session/ > /dev/null 2>&1; do sleep 1; done' cd react && npm run test:integration @$(MAKE) docker-down @@ -41,6 +41,6 @@ test-all: test test-integration clean: docker compose -f docker-compose.test.yml down -v --remove-orphans 2>/dev/null || true - rm -rf django/src/djarea.egg-info django/dist django/build + rm -rf django/src/mizan.egg-info django/dist django/build rm -rf react/dist react/node_modules rm -f example/db.sqlite3 diff --git a/README.md b/README.md index 92670f2..9df30b1 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,84 @@ -# DJAREA +# mizan -A modern Django + React Framework for perfectionists with deadlines. +Django + React server functions framework. RPC, not REST. -Write a Pydantic function, add the @client decorator, use configurable **Shape** types for your models. - -Djarea generates the entire React client: all your type interfaces, function call hooks, autoatic JWT, and a simple `` to make it all work. - -No API routing, no serializers, no REST/CRUD bullshit. +You define Python functions. mizan generates typed React hooks. No API routes, no serializers, no endpoint boilerplate. ```python -@client -def current_user(request) -> UserShape: - return UserShape.query(lambda qs: qs.filter(pk=request.user.pk))[0] +# Django +@client(context='global') +def current_user(request) -> UserOutput: + return UserOutput(email=request.user.email) ``` - ```tsx -const user: UserShape = useCurrentUser() // typed, cached, SSR-hydrated +// React (generated) +const user = useCurrentUser() // typed, SSR-hydrated, auto-refreshed ``` -The **Function** is the API contract. The **Shape** is the query. The hook is the artifact. That's it. +## Packages -Starts with session auth and upgrades to JWT on login. **It just works**. +| Package | Path | Install | +|---------|------|---------| +| `mizan` (Python) | `django/` | `uv add "mizan[channels] @ git+..."` | +| `@rythazhur/mizan` (TypeScript) | `react/` | `npm install @rythazhur/mizan@git+...` | -## What Djarea does - -A `@client` function in Django becomes a callable hook in React. The function's type signature orchestrates the entire pipeline for you — input validation, output serialization, TypeScript interfaces, and SQL projection. - -```python -class ArticleShape(Shape[Article]): - id: int | None = None - title: str - author: FlatAuthorShape - tags: list[TagShape] = [] -``` - -One Djarea **Shape** does three things simultaneously: -- Defines the Pydantic model for validation and serialization -- Generates a django-readers spec for a lean, field-scoped SQL query -- Produces the TypeScript interface on the React side - -Shapes are your codebase's **single source of truth** for backend/frontend data transfer. - -## Quick start +## Quick Start ### 1. Django setup ```python # settings.py INSTALLED_APPS = [ - "djarea", + "mizan", "myapp", ] # urls.py from django.urls import include, path urlpatterns = [ - path("api/djarea/", include("djarea.urls")), + path("api/mizan/", include("mizan.urls")), ] # asgi.py (for WebSocket support) -from djarea import wrap_asgi +from mizan import wrap_asgi from django.core.asgi import get_asgi_application application = wrap_asgi(get_asgi_application()) ``` -### 2. Define your client functions +### 2. Define server functions ```python -# myapp/clients.py -from djarea.client import client -from djarea.shapes import Shape +# myapp/mizan_clients.py +from django.http import HttpRequest +from mizan.client import client +from mizan.setup.registry import register from pydantic import BaseModel class EchoOutput(BaseModel): message: str @client -def echo(request, text: str) -> EchoOutput: +def echo(request: HttpRequest, text: str) -> EchoOutput: return EchoOutput(message=text) + +register(echo, "echo") ``` -Functions in `clients.py` are discovered automatically — same convention as `models.py`. +### 3. Register in apps.py -### 3. Generate TypeScript +```python +class MyAppConfig(AppConfig): + name = "myapp" -To get your generated React client, set this up in your frontend root: + def ready(self): + import myapp.mizan_clients # noqa: F401 +``` -```javascript -// django.config.mjs +### 4. Generate TypeScript + +```bash +# django.config.mjs export default { source: { django: { @@ -100,23 +90,27 @@ export default { } ``` -Run this command everytime your client needs updating. You can also throw this it on a file watcher pointed at your backend code: - ```bash -npx djarea-generate +npx mizan-generate ``` -### 4. Use in React +This produces typed hooks, a typed provider, form hooks with Zod validation, and channel hooks. + +### 5. Use in React ```tsx -import { DjangoContext, useEcho, useCurrentUser, DjangoError } from '@/api' +// layout.tsx +import { DjangoContext } from '@/api' -// layout.tsx — one provider, handles everything export default function Layout({ children }) { return {children} } +``` +```tsx // page.tsx +import { useEcho, useCurrentUser, DjangoError } from '@/api' + function MyComponent() { const user = useCurrentUser() const echo = useEcho() @@ -127,80 +121,91 @@ function MyComponent() { console.log(result.message) // typed } catch (e) { if (e instanceof DjangoError) { - console.log(e.code) // NOT_FOUND, VALIDATION_ERROR, etc. - e.getFieldErrors('email') // field-level errors + console.log(e.code) // NOT_FOUND, VALIDATION_ERROR, etc. } } } } ``` -## Shapes +## Features -Shapes are Djarea's data protocol. A Shape defines exactly which fields to select from the database, validated through Pydantic and projected through django-readers. Different views get different Shapes — same model, different queries. +| Backend | Frontend (generated) | Transport | +|---------|---------------------|-----------| +| `@client` | `useXxx()` | HTTP | +| `@client(context='global')` | `useXxx()` + SSR hydration | HTTP | +| `@client(context='local')` | `useXxx()` with params | HTTP | +| `@client(websocket=True)` | `useXxx()` | WebSocket RPC | +| `@client(auth=True\|'staff'\|callable)` | Auth errors as `DjangoError` | HTTP | +| `mizanFormMixin` | `useXxxForm()` + Zod validation | HTTP | +| `ReactChannel` | `useXxxChannel()` | WebSocket | +| `@compose(...)` | Combined providers | varies | -```python -# Full detail page — joins books with chapters -class AuthorDetailShape(Shape[Author]): - id: int | None = None - name: str - bio: str - books: list[BookShape] = [] +## Architecture -# Dropdown menu — two columns, no joins -class FlatAuthorShape(Shape[Author]): - id: int | None = None - name: str +``` +React app + └─ ← generated provider (includes ChannelProvider) + ├─ useCurrentUser() ← generated context hook (SSR-hydrated) + ├─ useEcho() ← generated function hook + ├─ useContactForm() ← generated form hook (Zod + server validation) + └─ useChatChannel() ← generated channel hook (WebSocket) + │ + ├─ HTTP: POST /api/mizan/call/ { fn: "echo", args: { text: "hi" } } + └─ WS: { action: "rpc", fn: "echo", args: { text: "hi" } } + │ + Django executor + ├─ Pydantic input validation + ├─ Auth check (session, JWT, or custom) + ├─ Function execution + └─ Pydantic output serialization ``` -```python -# Detail page: SELECT id, name, bio + prefetch books -authors = AuthorDetailShape.query() +The generated `DjangoContext` is the **only provider** needed. It wraps `mizanProvider` + `ChannelProvider` and handles session init, CSRF, context auto-fetching, and WebSocket connection. -# Dropdown: SELECT id, name. That's it. -authors = FlatAuthorShape.query() +## Code Generation + +`npx mizan-generate` reads Django schemas (no running server needed) and produces: + +| File | Contents | +|------|----------| +| `generated.mizan.ts` | Pydantic model types (via openapi-typescript) | +| `generated.django.tsx` | `DjangoContext` provider + all typed hooks | +| `generated.django.server.ts` | SSR hydration helper (`getDjangoHydration`) | +| `generated.forms.ts` | Form hooks with Zod schemas (`useContactForm`, etc.) | +| `generated.channels.ts` | Channel message types | +| `generated.channels.hooks.tsx` | Channel hooks (`useChatChannel`, etc.) | +| `index.ts` | Consolidated re-exports | + +## Error Handling + +All errors from server functions are thrown as `DjangoError`: + +```tsx +try { + await echo({ text: 'hello' }) +} catch (e) { + if (e instanceof DjangoError) { + e.code // 'NOT_FOUND' | 'VALIDATION_ERROR' | 'UNAUTHORIZED' | 'FORBIDDEN' | ... + e.message // Human-readable message + e.details // Field-level validation errors, etc. + e.isAuthError() + e.isValidationError() + e.getFieldErrors('email') + } +} ``` -Shapes also support diffing. When the frontend sends state back, the diff system compares incoming data against the current database state and tells you exactly what changed: - -```python -@client -def update_articles(request, articles: list[ArticleShape]) -> dict: - for article, diff in ArticleShape.diff_many(articles): - if diff.is_new: - create_article(article) - elif diff.changed: - update_fields(article, diff.changed) - for tag in diff.tags.created: - add_tag(article, tag) - for tag_id in diff.tags.deleted: - remove_tag(article, tag_id) - return {"ok": True} -``` - -One query fetches all current state. The diff is per-field and per-nested-relation. Your service code only touches what actually changed. - -## The `@client` decorator - -The decorator controls transport, caching, auth, and SSR behavior: - -| Decorator | React hook | What it does | -|-----------|-----------|--------------| -| `@client` | `useEcho()` | HTTP call, returns typed result | -| `@client(context='global')` | `useCurrentUser()` | Fetched once, cached in context, SSR-hydrated | -| `@client(context='local')` | `useArticle({ id })` | Cached per unique params | -| `@client(websocket=True)` | `useSearch()` | Runs over WebSocket instead of HTTP | -| `@client(auth=True)` | — | Requires authentication | -| `@client(auth='staff')` | — | Requires staff status | -| `@client(auth=my_check)` | — | Custom auth callable | +Error codes: `NOT_FOUND`, `VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `BAD_REQUEST`, `INTERNAL_ERROR`, `NOT_IMPLEMENTED`. ## Forms -Django forms become typed React hooks with client-side Zod validation: +Django forms get typed React hooks with client-side Zod validation: ```python -class ContactForm(DjareaFormMixin, forms.Form): - djarea = DjareaFormMeta( +# Django +class ContactForm(mizanFormMixin, forms.Form): + mizan = mizanFormMeta( name="contact", title="Contact Us", submit_label="Send", @@ -216,22 +221,22 @@ class ContactForm(DjareaFormMixin, forms.Form): ``` ```tsx +// React (generated) const form = useContactForm() -form.schema // field metadata, title, submit label +form.schema // { fields: { name: {...}, email: {...} }, title, submit_label } form.data // { name: '', email: '', message: '' } form.set('email', v) // typed setter form.errors // field-level errors (Zod + server) form.submit() // → { success: true, data: { sent: true } } ``` -Zod schemas are generated from the Django form definition. Validation runs client-side first, server-side second. No duplicated validation logic. - ## Channels WebSocket channels with typed messages: ```python +# Django class ChatChannel(ReactChannel): class Params(BaseModel): room: str @@ -252,6 +257,7 @@ class ChatChannel(ReactChannel): ``` ```tsx +// React (generated) const chat = useChatChannel({ room: 'general' }) chat.status // 'connecting' | 'connected' | 'disconnected' @@ -259,111 +265,32 @@ chat.messages // ChatDjangoMessage[] chat.send({ text: 'hello' }) ``` -## Architecture - -``` -React app - └─ ← generated provider (session, CSRF, WebSocket) - ├─ useCurrentUser() ← context hook (SSR-hydrated) - ├─ useEcho() ← function hook - ├─ useContactForm() ← form hook (Zod + server validation) - └─ useChatChannel() ← channel hook (WebSocket) - │ - ├─ HTTP: POST /api/djarea/call/ { fn: "echo", args: { text: "hi" } } - └─ WS: { action: "rpc", fn: "echo", args: { text: "hi" } } - │ - Django executor - ├─ Pydantic input validation - ├─ Auth check - ├─ Function execution - └─ Pydantic output serialization -``` - -All transport goes through a single endpoint. The generated `DjangoContext` is the only provider. It handles session init, CSRF, context auto-fetching, and WebSocket connection. - -## Code generation - -`npx djarea-generate` reads Django schemas at build time (no running server) and produces: - -| File | Contents | -|------|----------| -| `generated.djarea.ts` | Pydantic model types | -| `generated.django.tsx` | `DjangoContext` provider + typed hooks | -| `generated.django.server.ts` | SSR hydration helper | -| `generated.forms.ts` | Form hooks with Zod schemas | -| `generated.channels.ts` | Channel message types | -| `generated.channels.hooks.tsx` | Channel hooks | -| `index.ts` | Re-exports | - -## Error handling - -All errors from server functions throw as `DjangoError`: - -```tsx -if (e instanceof DjangoError) { - e.code // 'NOT_FOUND' | 'VALIDATION_ERROR' | 'UNAUTHORIZED' | ... - e.message // human-readable - e.details // field-level validation errors - e.isAuthError() - e.isValidationError() - e.getFieldErrors('email') -} -``` - -## Why RPC instead of REST - -REST exposes your database tables as CRUD endpoints and pushes business logic to the frontend. "Submit an application" becomes PATCH one resource, POST another, PUT a third — choreographed by client code. - -Djarea keeps business logic on the server. You write functions that do things. The frontend calls them. The server knows what "submit" means. The client doesn't need to. - -If you delete the frontend of a REST app, your backend is a database. If you delete the frontend of a Djarea app, your backend still has your entire application logic. - -## Packages - -| Package | Install | -|---------|---------| -| `djarea` (Python) | `pip install djarea` | -| `@rythazhur/djarea` (TypeScript) | `npm install @rythazhur/djarea` | - -For WebSocket support: `pip install "djarea[channels]"` - ## Testing ```bash -# Django -cd django && uv run pytest +# Django unit tests +cd django && uv sync --extra dev --extra channels && uv run pytest -# React +# React unit tests cd react && npm test -# E2E (Playwright, real browser + real backend) +# E2E integration tests (real browser, real backend) docker compose -f docker-compose.test.yml up -d -cd e2e/harness && npx djarea-generate && npx playwright test +cd e2e/harness && npm install && npx mizan-generate && npx vite --port 5174 & +npx playwright test -# Everything +# All at once make test-all ``` -## Project structure +## Project Structure ``` -djarea/ - django/ Python package - react/ TypeScript package - example/ Integration test backend - e2e/ Playwright E2E tests +mizan/ + django/ Python package (mizan) + react/ TypeScript package (@rythazhur/mizan) + example/ Integration test backend (Docker) + desktop/ PyWebView desktop test app + e2e/ Playwright E2E tests + React harness Makefile Test orchestration ``` - -## Disclosure - -Djarea was developed with the assistance of IDE AI Assistance and later with Claude Code. - -The architecture, design decisions, developer experience standards and technical direction are mine. I've been programming for 16 years and have a lot of opinions! - -DX ideas are inspired by the amazing work of these projects and the hardworking folks behind them: -- Django Ninja -- Django Readers -- Django RAPID Architecture -- React -- Next.js \ No newline at end of file diff --git a/desktop/app.py b/desktop/app.py index 604f858..0492959 100644 --- a/desktop/app.py +++ b/desktop/app.py @@ -1,9 +1,9 @@ #!/usr/bin/env python """ -Djarea Desktop — PyWebView + Django local RPC. +mizan Desktop — PyWebView + Django local RPC. Starts a local Django ASGI server and opens a native desktop window. -All communication between the UI and backend uses Djarea server functions. +All communication between the UI and backend uses mizan server functions. """ import os @@ -63,7 +63,7 @@ def main(): base_url = f"http://{host}:{port}" - if not wait_for_server(f"{base_url}/api/djarea/session/"): + if not wait_for_server(f"{base_url}/api/mizan/session/"): print("ERROR: Django server failed to start", file=sys.stderr) sys.exit(1) @@ -83,7 +83,7 @@ def main(): import webview window = webview.create_window( - title="Djarea Desktop", + title="mizan Desktop", url=base_url, width=1024, height=768, diff --git a/desktop/backend/asgi.py b/desktop/backend/asgi.py index a7a5338..2852595 100644 --- a/desktop/backend/asgi.py +++ b/desktop/backend/asgi.py @@ -6,8 +6,8 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") django.setup() from django.core.asgi import get_asgi_application -from djarea import wrap_asgi +from mizan import wrap_asgi -import backend.djarea_clients # noqa: F401 +import backend.mizan_clients # noqa: F401 application = wrap_asgi(get_asgi_application()) diff --git a/desktop/backend/djarea_clients.py b/desktop/backend/djarea_clients.py index acb1b2c..80e7993 100644 --- a/desktop/backend/djarea_clients.py +++ b/desktop/backend/djarea_clients.py @@ -1,7 +1,7 @@ """ Desktop RPC server functions. -Tests Djarea's appropriateness for desktop apps: +Tests mizan's appropriateness for desktop apps: - Local file system access - SQLite CRUD - System introspection @@ -20,10 +20,10 @@ from pathlib import Path from django.http import HttpRequest from pydantic import BaseModel -from djarea.client import client -from djarea.channels import ReactChannel -from djarea.setup.registry import register -from djarea.channels import register as register_channel +from mizan.client import client +from mizan.channels import ReactChannel +from mizan.setup.registry import register +from mizan.channels import register as register_channel # ============================================================================= @@ -40,12 +40,12 @@ class SystemInfoOutput(BaseModel): home_dir: str cwd: str cpu_count: int - djarea_version: str + mizan_version: str @client(websocket=True) def system_info(request: HttpRequest) -> SystemInfoOutput: - import djarea + import mizan return SystemInfoOutput( os_name=platform.system(), @@ -56,7 +56,7 @@ def system_info(request: HttpRequest) -> SystemInfoOutput: home_dir=str(Path.home()), cwd=os.getcwd(), cpu_count=os.cpu_count() or 1, - djarea_version=getattr(djarea, "__version__", "dev"), + mizan_version=getattr(mizan, "__version__", "dev"), ) @@ -114,16 +114,20 @@ def list_files(request: HttpRequest, directory: str = "~") -> ListFilesOutput: entries = [] try: - for entry in sorted(dir_path.iterdir(), key=lambda e: (not e.is_dir(), e.name.lower())): + for entry in sorted( + dir_path.iterdir(), key=lambda e: (not e.is_dir(), e.name.lower()) + ): try: stat = entry.stat() - entries.append(FileEntry( - name=entry.name, - path=str(entry), - is_dir=entry.is_dir(), - size=stat.st_size if not entry.is_dir() else 0, - modified=datetime.fromtimestamp(stat.st_mtime).isoformat(), - )) + entries.append( + FileEntry( + name=entry.name, + path=str(entry), + is_dir=entry.is_dir(), + size=stat.st_size if not entry.is_dir() else 0, + modified=datetime.fromtimestamp(stat.st_mtime).isoformat(), + ) + ) except (PermissionError, OSError): continue except PermissionError: @@ -268,7 +272,9 @@ register(list_notes, "list_notes") @client(websocket=True) -def create_note(request: HttpRequest, title: str, content: str = "", pinned: bool = False) -> NoteOutput: +def create_note( + request: HttpRequest, title: str, content: str = "", pinned: bool = False +) -> NoteOutput: from backend.models import Note note = Note.objects.create(title=title, content=content, pinned=pinned) @@ -403,7 +409,7 @@ def app_info(request: HttpRequest) -> AppInfoOutput: from django.conf import settings return AppInfoOutput( - app_name="Djarea Desktop", + app_name="mizan Desktop", uptime_seconds=round(time.time() - _start_time, 2), db_path=str(settings.DATABASES["default"]["NAME"]), pid=os.getpid(), diff --git a/desktop/backend/settings.py b/desktop/backend/settings.py index bd01eca..4a37a6d 100644 --- a/desktop/backend/settings.py +++ b/desktop/backend/settings.py @@ -1,5 +1,5 @@ """ -Django settings for the Djarea desktop integration test app. +Django settings for the mizan desktop integration test app. Runs entirely local: SQLite database, in-memory channel layer, no external services required. diff --git a/desktop/backend/urls.py b/desktop/backend/urls.py index ad66c10..38e7718 100644 --- a/desktop/backend/urls.py +++ b/desktop/backend/urls.py @@ -27,7 +27,7 @@ def serve_dist(request, path="index.html"): urlpatterns = [ - path("api/djarea/", include("djarea.urls")), + path("api/mizan/", include("mizan.urls")), re_path(r"^(?Passets/.+)$", serve_dist), path("favicon.ico", serve_dist, {"path": "favicon.ico"}), path("", serve_dist), diff --git a/desktop/frontend/index.html b/desktop/frontend/index.html index 5b30e6d..66412a7 100644 --- a/desktop/frontend/index.html +++ b/desktop/frontend/index.html @@ -3,7 +3,7 @@ - Djarea Desktop + mizan Desktop