Metadata-Version: 2.1
Name: create-star-app
Version: 1.28.0
Summary: Flask architecture scaffolding tool — generates core server files for the star app architecture.
Author-email: "Adrian Anton D. Ladia" <ladiaadrian@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/AALadia/create-star-app
Project-URL: Repository, https://github.com/AALadia/create-star-app
Project-URL: Issues, https://github.com/AALadia/create-star-app/issues
Keywords: flask,scaffolding,boilerplate,code-generator
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Framework :: Flask
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Code Generators
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE

# create-star-app

Flask architecture scaffolding tool that generates core server files for the star app architecture.

## What it does

`create-star-app` is an interactive CLI that prompts for project configuration (name, port, timezone, GCP region) and generates a ready-to-use Flask server and Next.js client with all the boilerplate files pre-configured.

## Installation

```bash
pip install -e .
```

## Usage

Run the CLI from the directory where you want to create your project:

```bash
create-star-app
```

You will be prompted for:

- **Project name** (required)
- **Port** (default: 5000)
- **Timezone offset** (default: 8)
- **GCP region** (default: asia-east1)

The tool generates `server/` (Flask, MongoDB, Pub/Sub, pydantic models), `client/` (Next.js via create-next-app, with MUI and Firebase), `.vscode/`, `CLAUDE.md` and `.claude/`, then runs `AppCreator.py` once (with `ENVIRONMENT=localdev`).

Prerequisites for that first `AppCreator.py` run: the server requirements installed in the active Python, and `json2ts` on PATH (`npm install -g json-schema-to-typescript`).

## Development

```bash
pip install -e .
```

Then run directly:

```bash
python -m create_star_app.cli
```

## Shared files

This repo is the source of truth for the server files every Star App project keeps identical: the generic tests (`test_file_sizes.py`, `test_route_coverage.py`, `test_mock_drift.py`, ...), `servicePortCheck.py`, the Pub/Sub map tools, `sharedFileCheck.py` itself and the `tools/` job scripts. `fleet/sharedFiles.json` lists them, each `exact` or `regions` (only the `# >>> star-app shared: <name>` ... `# <<< star-app shared: <name>` regions are shared). A `# >>> star-app per-repo: <name>` block inside either keeps each project's own content. A project's `server/sharedFileCheck.py` and `test_sharedFiles.py` compare its copies with `../create-star-app` at its committed HEAD, rendered as scaffold.py renders them; with no checkout there, they skip with a reason.

To change a shared file, or fold back a fix made in one project:

1. Edit the template's `.tmpl` (a literal `$` is `$$`) or the `fleet/` file. `cd server && python sharedFileCheck.py --template-worktree` in a project previews the result.
2. Commit it here.
3. Run `cd server && python sharedFileCheck.py --write` in every fleet project and commit there.

Never edit one project's copy alone. `fleet/` sits outside the `create_star_app` package (package data is `templates/*`), so the manifest and the fleet-only files (`apiNamespace.py`, `test_objects_package_exports.py`) never ship to PyPI.

Not enforced yet, because the copies have diverged (converge one, then add it to the manifest; until then fold back by hand): `AppCreator.py`, `conftest.py`, `route_config.py`, `pubSub.py`, `pubSubAdmin.py`, `mongoDb.py`, `AppConfig.py`, `roles.py`, `AuthHandler.py`, `utils.py`, `services/__abstractService.py`, `services/__request.py`, `services/__getIsProductionEnv.py` and `services/AllServices.py`, `checkPubSubTopics.py`, the typed-response test files, `Dockerfile` and `requirements.txt`.

## Changelog

### 1.28.0 — response check in localdev only

- **`route_config` checks a response in localdev only**: there (and so in tests) a mismatch still raises `ValueError`, with the mutation guard. Every other environment skips the check entirely (no validation, no deep copy, no log), because validating every response cost every production call time. The `responseModelMismatch` warning, its `loc` masking (`_masked_loc` and its helpers) and the `logging`/`annotationLeaves` imports they needed are gone; the route's value is still returned unchanged.
- **conftest**: the `responseCheckWarnsOnly` fixture is now `responseCheckOff` (it turns the localdev check off for a test that pins a shape no model describes), and `mismatchRecords` is gone.
- **`test_route_response_models.py`**: the tests outside localdev pin that the check never validates, copies or logs; the warning and masking tests are gone.

### Upgrading a 1.27.0 project to 1.28.0

Take `_check_response_model`, the wrapper's check call and the imports from a fresh scaffold's `route_config.py`. In `conftest.py`, rename the `responseCheckWarnsOnly` fixture to `responseCheckOff` (and every test that uses it), take its comment from the scaffold and delete `mismatchRecords`. In `test_route_response_models.py`, take the scaffold's `_assertCheckSkipped` helper (it needs `from types import SimpleNamespace`) and its `...skippedOutsideLocaldev`, `test_check_neverValidatesNorCopiesOutsideLocaldev` and `test_responseCheckOff_fixtureTurnsTheCheckOff` tests in place of the warning tests, which you delete with any helper only they used.

### 1.27.0 — strict Pub/Sub contract check

- **Compat is strict**: every payload/handler mismatch (`undeclared`, `nullable`, `literal`, `numericNarrowing`, `forbiddenExtra`) fails `test_pubSubMap.py` and `python pubSubMap.py` in both repos of the flow, and nothing suppresses one: fix it at its source, the publisher's payload model or the consumer's handling (`--explain <handler>` shows both sides). `PUBSUB_CONTRACT_GAPS` is retired: defining it in any form, even `{}`, is a hatch error. `PUBSUB_MESSAGE_MODELS` and `PUBSUB_PAYLOAD_MODELS` stay; they declare models and suppress nothing. A flow's verdict is `ok`, `issues` or `unverified` (`knownGap` and `notEnforced` are gone), and the map's `formatVersion` is 3.

### Upgrading a 1.26.0 project to 1.27.0

Run `cd server && python sharedFileCheck.py --write` (it takes the new map tools and `test_pubSubMap.py`), then delete `PUBSUB_CONTRACT_GAPS` and its comment from `PubSubRequests.py` and take a fresh scaffold's comment above `PUBSUB_MESSAGE_MODELS`. Run `python pubSubMap.py`, fix every compat issue it reports at its source (in whichever repo owns it), then regenerate and commit the map in every repo of the fleet (`--write-all` regenerates them at once).

### 1.26.0 — Pub/Sub map and shared-file check

- **Pub/Sub map**: `pubSubMap.py` (with `pubSubMapScan.py`, `pubSubMapContract.py` and `pubSubMapSection.py`; pure stdlib, Python 3.10, no app import) writes `server/pubsub_map.json` and `server/PUBSUB_MAP.md`: every `@pubSubDecorator` publisher in the fleet, each consumer's handler, the payload paths each handler reads, and whether the publisher's types satisfy them. Siblings are found through every repo's `services/*.py` (`localRepoDirName`) and read at a committed git ref (this branch's name, else `main`); a missing one falls back to the checked-in snapshot and gets a gitignored `server/services/<name>.notFound.md` note (`--write` and the AppCreator hook refuse while a sibling this repo needs is neither checked out nor in the snapshot). `python pubSubMap.py` exits 0 only when the map is fresh and clean for this repo, equivalent to `test_pubSubMap.py` (1 = stale or a failing problem, 2 = an error); `--write` regenerates, `--explain <topic|handler|publisherFn>` prints live `file:line`, and `--repo name=path[@ref]` / `--worktree-siblings` override where siblings are read.
- **`--write-all`** regenerates the map here and in every sibling checkout on the same branch, with this repo's generator in-process (a sibling's code is never run). It skips other-branch, detached, missing and not-yet-adopted siblings with a reason, never stages or commits, and ends by listing the repos to commit.
- **`test_pubSubMap.py`** fails on a stale map, a consumer with no handler, a handler with no publisher, a message contract it cannot resolve, a publisher `pubSubAdmin.py` cannot see, a hand `publishMessage` on a topic that is not a decorated function's, a dead escape hatch or gap, and a compat issue (`undeclared`, `nullable`, `literal`, `numericNarrowing`, `forbiddenExtra`) on a consumer that enforces compat. Static and type-level only: the `test_pubSubContracts.py` replay stays the ground truth.
- **AppCreator** runs `writeMap()` at the end of a full generation (never with `--constants-only`), so a new scaffold's first run writes the initial map; a hard map error stops the run.
- **`PubSubRequests.py`** defines empty `PUBSUB_MESSAGE_MODELS` (the hatch for a handler the map cannot follow) and `PUBSUB_CONTRACT_GAPS` (`{'<handler>': {'<path>': {'<issue>': '<reason>'}}}`), so a new project enforces compat from the start (1.27.0 retires `PUBSUB_CONTRACT_GAPS`: compat is always enforced). A publisher's module may declare `PUBSUB_PAYLOAD_MODELS = {'<fn>': '<Model>'}`.
- **`sharedFileCheck.py`** and **`test_sharedFiles.py`** hold the files `fleet/sharedFiles.json` lists to this repo's committed HEAD (see "Shared files"). `--write` takes the template's version, `--template-worktree` compares with uncommitted template work, `--template <dir>` names another checkout.
- **Markers**: `test_pubSubContracts.py` marks its "Shared AST helpers" and "Static contract tests" regions, and `test_ifAllServicesMatchConsumerProjectName.py` a per-repo `SERVICES_WITHOUT_MOCK_DATA_YET` block.
- **`.gitignore`** ignores `server/services/*.notFound.md`.
- **`fleet/`** (new, outside the package): `sharedFiles.json` and the fleet-only `apiNamespace.py` and `test_objects_package_exports.py`.

### Upgrading a 1.25.0 project to 1.26.0

1. Mark the shared parts. In `server/test_pubSubContracts.py`, wrap the "Shared AST helpers" block and the static tests in `# >>> star-app shared: Shared AST helpers` / `# <<< star-app shared: Shared AST helpers` and `# >>> star-app shared: Static contract tests` / `# <<< star-app shared: Static contract tests`, and move anything project-specific (a wipe helper, a collection list) below them. In `test_ifAllServicesMatchConsumerProjectName.py`, wrap the `SERVICES_WITHOUT_MOCK_DATA_YET` line in `# >>> star-app per-repo: SERVICES_WITHOUT_MOCK_DATA_YET` / `# <<< star-app per-repo: SERVICES_WITHOUT_MOCK_DATA_YET`.
2. Copy `server/sharedFileCheck.py` from a fresh scaffold (not the raw `.tmpl`, which escapes `$`), check this repo out at `../create-star-app`, and run `cd server && python sharedFileCheck.py --write`: it writes the map tools and both new tests. Without a checkout, copy `pubSubMap.py`, `pubSubMapScan.py`, `pubSubMapContract.py`, `pubSubMapSection.py`, `test_pubSubMap.py` and `test_sharedFiles.py` from the fresh scaffold too.
3. Add the `writeMap` hook to the end of `AppCreator.py`'s `__main__`, the `server/services/*.notFound.md` line to `.gitignore`, and the two dicts (with their comments) to `PubSubRequests.py`. Defining `PUBSUB_CONTRACT_GAPS` turns compat enforcement on: fix, or record with a reason, every issue `python pubSubMap.py` reports, or leave the dict out until you can. Going on to 1.27.0, add only `PUBSUB_MESSAGE_MODELS`: 1.27.0 retires `PUBSUB_CONTRACT_GAPS`.
4. Run `cd server && python pubSubMap.py --write` and commit `pubsub_map.json` and `PUBSUB_MAP.md`. In a fleet, regenerate and commit them in every repo, with the same branch name checked out in each (siblings are read at this repo's branch name if they have it, else at `main`): `--write-all` regenerates every sibling checkout on the branch at once, and the commits stay manual.

### 1.25.0 — mock drift test

- **`test_mock_drift.py`** fails while a test's fake database returns a hand-written document. It scans every git-listed test file under `server/` (and `conftest.py`) for a db fake (`<...>.<db method>.return_value` / `.side_effect`, `MagicMock(return_value=...)` on a db method, `patch('<...>.db.<db method>', return_value=...)`, `patch.object(<...>, '<db method>', return_value=...)`) whose value is a dict literal, a `dict(...)` call, a list, tuple or `iter(...)` of them, or a name the same function (else the module) binds to one. Such a fake keeps its old shape when a model renames or removes a field, so the test passes against data the real database never returns. The failure lists each fake and how to fix it.
- **`fakeDoc(Model, **fields)`** in `conftest.py` builds a fake document through its model: it validates the fields, refuses any key the model does not declare (nested models and lists of them included) and returns the dump by alias (`_id`, `_version`, defaults). A value the model rewrites (`''` -> `None`) arrives rewritten; set it on the returned dict to feed a raw one.
- **Exemption**: a deliberately raw document (a legacy shape a normalization test repairs, a malformed record) keeps its dict and ends the fake's statement, or the statement binding the name, with `# raw-doc: <reason>` (a reason of at least 10 characters).
- **Known limits** (convert these by hand): documents returned from the body of a `def` used as a `side_effect`, and documents passed into a helper as parameters. Not scanned: anything in `venv`, `.venv`, `node_modules` or `__pycache__`.

### Upgrading a 1.24.0 project to 1.25.0

Copy `server/test_mock_drift.py` from a fresh scaffold (it is the same in every project; from 1.26.0 on, `cd server && python sharedFileCheck.py --write` takes it), and paste the `# --- Fake documents ---` block (`_declaredKeys`, `_unknownKeys`, `fakeDoc`) from its `server/conftest.py` into yours below `modelKeys`, adding `from pydantic import BaseModel, RootModel`. Run the test: an existing project usually fails at first. Convert each fake it lists to `fakeDoc(<Model>, ...)` (a repo factory such as `unitDoc` can wrap it), or mark a deliberate one `# raw-doc: <reason>`. Change test setup only, never an assert; a test that fails once its fake goes through the model was hiding a real mismatch, so report it.

### 1.24.0 — file size test

- **`test_file_sizes.py`** fails while any hand-written source file outside `client/` (`.py .ts .tsx .js .jsx .mjs .cjs .css`) is over 1,500 lines. The failure is a brief for the smallest oversized file: an outline with line ranges, how to split that kind of file (`ApiRequests.py` into `server/actions/<Name>Actions.py` mixins, an oversized `*Actions.py` into topic mixins, `objects.py` into an `objects/` package), the rules (move code only, retarget patches, never raise the limit or change an assert) and the checks that mark the split done (unchanged generated code, unchanged test count, full suite). Not checked: the client folder, files AppCreator generates (paths read from AppCreator.py), files whose name starts with `test`, and anything in `venv`, `.venv`, `node_modules`, `.next` or `__pycache__`.
- **`test_route_coverage.py`** fails when a route (a public `@route_config` method on ApiRequests or a PubSubRequests push handler) is not called by any test, as `api.<route>(...)` or a request to `/<route>`, outside `pytest.raises`. It also fails when a test calls a route but asserts nothing (no `assert`, no `pytest.raises`, no `assert*` helper or mock `assert_*`).

### Upgrading a 1.23.0 project to 1.24.0

Copy `server/test_file_sizes.py` from a fresh scaffold (it is the same in every project; from 1.26.0 on, `cd server && python sharedFileCheck.py --write` takes it) and run it. An existing project usually fails at first; split the files it names one at a time.

### 1.23.0 — typed API responses

Ports the typed-responses redesign (ecommerce, SalesApp, accounting, warehouse) into the templates:

- **Every route declares a `responseModel`** (`@route_config(responseModel=<objects.py model>)`); AppCreator refuses to generate without one (enforcement on from the start; `PubSubRequests` handlers are exempt). `route_config` checks every response against it: it raises in localdev (so in tests) and elsewhere logs `responseModelMismatch` with `type`/`loc` only (dict keys masked as `<key>`), never values, and never copies outside localdev.
- **Typed client**: `ServerRequests.ts` methods return `Promise<ApiResponse<Model>>`; the generated `ApiResponse.ts` is the `ApiSuccess<T>` / `ApiFailure` union built from the `STATUS_*` constants; `RouteResponseModels`, `RouteRequestBodies` and `PublicRoute` maps; imports derived from the routes; no `any`.
- **AppCreator**: schemas from the cached real `objects` module (BaseModel classes only, no silent skips), closed `$defs`, `$ref` siblings wrapped in `allOf`, the `X1` collision guard, stale-schema deletion, `json2ts` via `subprocess.run(check=True)`, `validateRoutes()` before any write, param guards (no date/datetime/time, `= None` needs Optional, renderable containers only, `objects.py` classes only), the secret-key guard, and the `CROSS_REPO_BARE_RESPONSE_ROUTES` allowlist (empty).
- **Generated handlers** bind the request body unconditionally (a body that is not a JSON object reads as {}, so a route's params are None and it answers with its own envelope, never a 500), refuse an object or a list for a scalar param (a `{"$ne": null}` never reaches a query), log which of their own params a POST sent (never values), and a login route logs no request data. Pub/Sub push handlers parse the envelope inside the try (a malformed push is a 400) and log the messageId and the message's keys.
- **route_config** refuses `roleAccess` without `jwtRequired=True`; the mismatch log masks int dict keys too.
- **Local runs bind 127.0.0.1**; `LOCAL_BIND_HOST=0.0.0.0` in `server/.env` opens the server to the LAN, with a startup warning. Scaffolding writes a random `JWT_SECRET_KEY`, and app.py refuses one under 32 characters (or the old placeholder) outside localdev. Flask's `MAX_CONTENT_LENGTH` is 16 MB.
- **mongoDb.py** prints a query's keys, never its values.
- **objects.py**: copy-on-write empty-string coercion, `StoredDocumentKeys`, `validate_route_response`, `_projection_of` / `ProjectedSubdocument` / `projectedCopy`, typed sample models (`SampleTodoView`, the `SampleTodoRow` row model, response wrappers).
- **Sign-in**: `loginWithGoogle` verifies the Firebase ID token (google-auth with a cached cert set: an RS256 token with a known key id only, at most one fetch attempt a minute, the last good certs kept for 6 hours while Google is unreachable, an answer of the wrong shape refused), requires a verified email and the google.com provider, and accepts a raw identity in localdev only; `devLogin` / `devGetUserList` are localdev only. The project id is `FIREBASE_PROJECT_ID` in `sharedConstants.py`, read by the server and `client/lib/firebase.ts`; while it is the placeholder, sign-in fails closed with a warning. The first superAdmin is an atomic one-time claim. The client's `AuthContext.signInWithGoogle` sends `{ idToken }`.
- **utils**: strict `appDayStart`, `appDayBounds`, `parseIsoInstant`.
- **Tests**: `conftest.py` refuses non-localdev and non-`*Scratch` databases, unsets the cloud DB variables (again after AppConfig loads `.env`) and installs `_LocalOnlyMongoClient` (sync, async and `pymongo.synchronous`) before any import, and holds the sign-in fixtures; new `test_route_response_models.py`, `test_auth_surface.py`, `test_route_responses.py` and `test_route_responses_http.py`.
- **Client**: narrows on `status === 200` with the generated types, `eslint.config.mjs` bans importing `schemas/ApiResponseSchema`, and the starter is lint-clean (`npm run lint`: 0 errors); `DevClickToComponent` renders in development only.
- **Docs**: CLAUDE.md, ARCHITECTURE.md, DO_NOT_DO.md, `.coderabbit.yaml`, `.vscode/tasks.json` and a project section appended to `.claude/context/TYPE_SAFETY.md`.
- **Breaking for the sample routes**: `getRoles` answers `{roles}`, `getUserRoleTypes` `{userRoleTypes}`, `devGetUserList` `{users}`, `getTodos` `{todos}`; `FirebaseUserObject` moved to `objects.py` and `AuthHandler.getOrCreateUser` became `signInIdentity`.
- **Deferred**:
  - `json_schema_serialization_defaults_required` (T5) together with validation-mode request-param TS types (T5b). Routes return raw dicts and the model rules allow omittable fields (`X = Field(None)`), so a defaults-required serialization schema would type keys the route can leave out as always present. A model always returned as a full `model_dump()` may opt in per model (documented in `objects.py`).
  - A guard against `@computed_field` on response models. pydantic lists a computed field as required in the serialization schema, so the TS type declares it, but a route returning a raw dict never sends it. The existing repos use computed fields, so it is documented (DO_NOT_DO.md, `objects.py`) rather than enforced.
  - Requiring an allowlist of bootstrap superAdmin emails outside localdev (the first sign-in on an empty Users still becomes superAdmin, now through an atomic claim).

### Upgrading a 1.22.0 project to 1.23.0

The scaffold overwrites every template file in place, so an existing project upgrades by hand. In this order:

a. **Server infrastructure.** Re-scaffold into a scratch directory with the same name and port, then copy over `route_config.py`, `AppCreator.py`, `conftest.py`, `utils.py` (the date parsers and annotation walkers), `AuthHandler.py`, the base of `objects.py` (from the top through `ApiResponse`, plus `UserView`, `FirebaseUserObject`), the `mongoDb.py` print changes, `sharedConstants.FIREBASE_PROJECT_ID`, the `'Bootstrap'` entry in `pubSubMockDataGenerator.py`'s `TEST_COLLECTIONS`, `services/__getIsProductionEnv.py`, and the four new `test_*.py` files. Copy the sign-in routes of `ApiRequests.py` (`loginWithGoogle`, `devGetUserList`, `devLogin` and the `_requireLocalDev` helper) and keep your own models and other routes.
b. **A `responseModel` on every route.** Add `responseModel=<objects.py model>` to every `ApiRequests` route: a `*View(StoredDocumentKeys, Model)` for a stored document, a row model with `projection=_projection_of(Row)` for a projected read, a `...Response` wrapper otherwise. AppCreator refuses to generate until every route has one. Give `roleAccess` routes `jwtRequired=True`.
c. **Wrap bare answers.** A route returning a list or a primitive now returns `{'key': value}`; update its callers (the sample routes: `getRoles` → `{roles}`, `getUserRoleTypes` → `{userRoleTypes}`, `devGetUserList` → `{users}`, `getTodos` → `{todos}`). Never reshape a `@pubSubDecorator` route or a route another service reads.
d. **Route params.** Replace `datetime`/`date` params with an ISO `str` parsed by `utils.appDayStart` / `appDayBounds` / `parseIsoInstant`, a bare `dict`/`list` with a model or a typed container, and a `= None` default without `| None`.
e. **Sign-in.** Put the Firebase project id in `sharedConstants.FIREBASE_PROJECT_ID` (or the `FIREBASE_PROJECT_ID` env var), have the client send `loginWithGoogle({ idToken })` (`AuthContext.signInWithGoogle`), and deploy the backend and frontend together: the old raw-identity login is refused outside localdev.
f. **Secrets and local runs.** Give every deployed environment a `JWT_SECRET_KEY` of at least 32 random characters before deploying (app.py refuses a weaker one outside localdev). Local runs now bind `127.0.0.1`; set `LOCAL_BIND_HOST=0.0.0.0` in `server/.env` only if devices on your network must reach it.
g. **Client and tests.** Copy `client/eslint.config.mjs`, `client/lib/AuthContext.tsx`, `client/lib/firebase.ts` and `client/app/layout.tsx`, run `ENVIRONMENT=localdev python AppCreator.py`, fix every `npx tsc --noEmit` error by narrowing on `res.status === 200` (no `res.data as X`), and run the tests with `ENVIRONMENT=localdev databaseName=test<projectName>Scratch pytest` (conftest refuses any other database).
