mizzle 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. mizzle-0.1.0/.gitignore +19 -0
  2. mizzle-0.1.0/LICENSE +21 -0
  3. mizzle-0.1.0/PKG-INFO +175 -0
  4. mizzle-0.1.0/README.md +146 -0
  5. mizzle-0.1.0/assets/icon-1024.png +0 -0
  6. mizzle-0.1.0/assets/icon-512.png +0 -0
  7. mizzle-0.1.0/assets/icon.svg +1 -0
  8. mizzle-0.1.0/assets/logo-dark.png +0 -0
  9. mizzle-0.1.0/assets/logo-dark.svg +1 -0
  10. mizzle-0.1.0/assets/logo.png +0 -0
  11. mizzle-0.1.0/assets/logo.svg +1 -0
  12. mizzle-0.1.0/assets/mark-1024.png +0 -0
  13. mizzle-0.1.0/assets/mark-512.png +0 -0
  14. mizzle-0.1.0/assets/mark-dark-1024.png +0 -0
  15. mizzle-0.1.0/assets/mark-dark-512.png +0 -0
  16. mizzle-0.1.0/assets/mark-dark.svg +1 -0
  17. mizzle-0.1.0/assets/mark.svg +1 -0
  18. mizzle-0.1.0/assets/social-preview.png +0 -0
  19. mizzle-0.1.0/examples/social/.env.example +4 -0
  20. mizzle-0.1.0/examples/social/README.md +49 -0
  21. mizzle-0.1.0/examples/social/migrations/0001_social_schema/migration.sql +88 -0
  22. mizzle-0.1.0/examples/social/migrations/0001_social_schema/snapshot.json +400 -0
  23. mizzle-0.1.0/examples/social/migrations/0002_create_table_bookmarks/migration.sql +17 -0
  24. mizzle-0.1.0/examples/social/migrations/0002_create_table_bookmarks/snapshot.json +477 -0
  25. mizzle-0.1.0/examples/social/migrations/meta/journal.json +18 -0
  26. mizzle-0.1.0/examples/social/mizzle.toml +6 -0
  27. mizzle-0.1.0/examples/social/pyproject.toml +52 -0
  28. mizzle-0.1.0/examples/social/social/__init__.py +0 -0
  29. mizzle-0.1.0/examples/social/social/app.py +311 -0
  30. mizzle-0.1.0/examples/social/social/models.py +69 -0
  31. mizzle-0.1.0/examples/social/social/schema.py +120 -0
  32. mizzle-0.1.0/examples/social/social/seed.py +154 -0
  33. mizzle-0.1.0/examples/social/tests/__init__.py +0 -0
  34. mizzle-0.1.0/examples/social/tests/test_api.py +133 -0
  35. mizzle-0.1.0/examples/social/uv.lock +531 -0
  36. mizzle-0.1.0/pyproject.toml +143 -0
  37. mizzle-0.1.0/release/mizzle-studio-0.1.0.zip +0 -0
  38. mizzle-0.1.0/src/mizzle/__init__.py +124 -0
  39. mizzle-0.1.0/src/mizzle/__main__.py +5 -0
  40. mizzle-0.1.0/src/mizzle/core/__init__.py +0 -0
  41. mizzle-0.1.0/src/mizzle/core/compiler.py +365 -0
  42. mizzle-0.1.0/src/mizzle/core/errors.py +78 -0
  43. mizzle-0.1.0/src/mizzle/core/naming.py +13 -0
  44. mizzle-0.1.0/src/mizzle/core/query/__init__.py +0 -0
  45. mizzle-0.1.0/src/mizzle/core/query/base.py +82 -0
  46. mizzle-0.1.0/src/mizzle/core/query/builder.py +205 -0
  47. mizzle-0.1.0/src/mizzle/core/query/insert.py +131 -0
  48. mizzle-0.1.0/src/mizzle/core/query/relational.py +271 -0
  49. mizzle-0.1.0/src/mizzle/core/query/returning.py +22 -0
  50. mizzle-0.1.0/src/mizzle/core/query/select.py +214 -0
  51. mizzle-0.1.0/src/mizzle/core/query/update.py +78 -0
  52. mizzle-0.1.0/src/mizzle/core/schema/__init__.py +0 -0
  53. mizzle-0.1.0/src/mizzle/core/schema/column.py +231 -0
  54. mizzle-0.1.0/src/mizzle/core/schema/constraints.py +85 -0
  55. mizzle-0.1.0/src/mizzle/core/schema/relations.py +191 -0
  56. mizzle-0.1.0/src/mizzle/core/schema/table.py +144 -0
  57. mizzle-0.1.0/src/mizzle/core/schema/types.py +61 -0
  58. mizzle-0.1.0/src/mizzle/core/snapshot.py +98 -0
  59. mizzle-0.1.0/src/mizzle/core/sql/__init__.py +0 -0
  60. mizzle-0.1.0/src/mizzle/core/sql/nodes.py +158 -0
  61. mizzle-0.1.0/src/mizzle/core/sql/operators.py +213 -0
  62. mizzle-0.1.0/src/mizzle/core/sql/raw.py +83 -0
  63. mizzle-0.1.0/src/mizzle/core/steps.py +283 -0
  64. mizzle-0.1.0/src/mizzle/db.py +237 -0
  65. mizzle-0.1.0/src/mizzle/dialects/__init__.py +0 -0
  66. mizzle-0.1.0/src/mizzle/dialects/base.py +62 -0
  67. mizzle-0.1.0/src/mizzle/dialects/postgres/__init__.py +87 -0
  68. mizzle-0.1.0/src/mizzle/dialects/postgres/compiler.py +128 -0
  69. mizzle-0.1.0/src/mizzle/dialects/postgres/ddl.py +280 -0
  70. mizzle-0.1.0/src/mizzle/dialects/postgres/driver.py +166 -0
  71. mizzle-0.1.0/src/mizzle/dialects/postgres/introspect.py +209 -0
  72. mizzle-0.1.0/src/mizzle/dialects/postgres/types.py +117 -0
  73. mizzle-0.1.0/src/mizzle/kit/__init__.py +27 -0
  74. mizzle-0.1.0/src/mizzle/kit/cli.py +424 -0
  75. mizzle-0.1.0/src/mizzle/kit/codegen.py +309 -0
  76. mizzle-0.1.0/src/mizzle/kit/config.py +182 -0
  77. mizzle-0.1.0/src/mizzle/kit/diff.py +385 -0
  78. mizzle-0.1.0/src/mizzle/kit/init.py +105 -0
  79. mizzle-0.1.0/src/mizzle/kit/journal.py +183 -0
  80. mizzle-0.1.0/src/mizzle/kit/migrate.py +96 -0
  81. mizzle-0.1.0/src/mizzle/kit/snapshot.py +156 -0
  82. mizzle-0.1.0/src/mizzle/pg.py +57 -0
  83. mizzle-0.1.0/src/mizzle/py.typed +0 -0
  84. mizzle-0.1.0/src/mizzle/pydantic.py +158 -0
  85. mizzle-0.1.0/src/mizzle/studio/__init__.py +0 -0
  86. mizzle-0.1.0/src/mizzle/studio/assets.py +276 -0
  87. mizzle-0.1.0/src/mizzle/studio/server.py +458 -0
  88. mizzle-0.1.0/src/mizzle/studio/ui.json +5 -0
  89. mizzle-0.1.0/tests/__init__.py +0 -0
  90. mizzle-0.1.0/tests/conftest.py +4 -0
  91. mizzle-0.1.0/tests/integration/__init__.py +0 -0
  92. mizzle-0.1.0/tests/integration/conftest.py +51 -0
  93. mizzle-0.1.0/tests/integration/test_database.py +240 -0
  94. mizzle-0.1.0/tests/integration/test_kit_cli.py +244 -0
  95. mizzle-0.1.0/tests/integration/test_relational.py +196 -0
  96. mizzle-0.1.0/tests/integration/test_roundtrip.py +202 -0
  97. mizzle-0.1.0/tests/integration/test_studio.py +128 -0
  98. mizzle-0.1.0/tests/models.py +34 -0
  99. mizzle-0.1.0/tests/typing/__init__.py +0 -0
  100. mizzle-0.1.0/tests/typing/check_types.py +66 -0
  101. mizzle-0.1.0/tests/unit/__init__.py +0 -0
  102. mizzle-0.1.0/tests/unit/conftest.py +39 -0
  103. mizzle-0.1.0/tests/unit/test_columns.py +102 -0
  104. mizzle-0.1.0/tests/unit/test_compiler.py +151 -0
  105. mizzle-0.1.0/tests/unit/test_errors.py +24 -0
  106. mizzle-0.1.0/tests/unit/test_kit.py +252 -0
  107. mizzle-0.1.0/tests/unit/test_mutations.py +192 -0
  108. mizzle-0.1.0/tests/unit/test_pydantic.py +110 -0
  109. mizzle-0.1.0/tests/unit/test_raw_py314.py +22 -0
  110. mizzle-0.1.0/tests/unit/test_select.py +177 -0
  111. mizzle-0.1.0/tests/unit/test_studio_assets.py +168 -0
  112. mizzle-0.1.0/tests/unit/test_tables.py +111 -0
  113. mizzle-0.1.0/uv.lock +919 -0
@@ -0,0 +1,19 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .coverage
5
+ htmlcov/
6
+ dist/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+
11
+ # Studio UI: local dev build, and the release manifest (written by release.yml)
12
+ src/mizzle/studio/static/
13
+ src/mizzle/studio/ui.json
14
+
15
+ # Local secrets (database URLs, passwords)
16
+ .env
17
+
18
+ # Local AI-assistant working notes (design specs, plans)
19
+ docs/superpowers/
mizzle-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mizzle contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
mizzle-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.5
2
+ Name: mizzle
3
+ Version: 0.1.0
4
+ Summary: Fully typed async ORM for PostgreSQL: SQL-like queries, schema-driven migrations and a data studio
5
+ Project-URL: Homepage, https://github.com/mizzlehq/mizzle
6
+ Project-URL: Repository, https://github.com/mizzlehq/mizzle
7
+ Project-URL: Issues, https://github.com/mizzlehq/mizzle/issues
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: async,asyncpg,migrations,orm,postgresql,query-builder,sql,typed
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Database
19
+ Classifier: Topic :: Database :: Front-Ends
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.12
22
+ Requires-Dist: asyncpg>=0.30
23
+ Provides-Extra: pydantic
24
+ Requires-Dist: pydantic>=2.7; extra == 'pydantic'
25
+ Provides-Extra: studio
26
+ Requires-Dist: starlette>=0.40; extra == 'studio'
27
+ Requires-Dist: uvicorn>=0.30; extra == 'studio'
28
+ Description-Content-Type: text/markdown
29
+
30
+ <p align="center">
31
+ <picture>
32
+ <source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
33
+ <img alt="mizzle" src="assets/logo.svg" width="320">
34
+ </picture>
35
+ </p>
36
+
37
+ # mizzle
38
+
39
+ A fully typed, async ORM for Python and PostgreSQL: write queries that read like SQL,
40
+ get typed rows back, and evolve your schema with generated migrations. If you know
41
+ [Drizzle](https://orm.drizzle.team) from TypeScript, mizzle will feel familiar.
42
+
43
+ - **Schema in Python classes.** Result rows are typed for pyright and mypy, with no code
44
+ generation.
45
+ - **Two query styles:** a SQL-like builder (`select`, `insert`, `update`, `delete`, joins,
46
+ CTEs, upserts) and relational queries that load nested data in one SQL statement.
47
+ - **Migration kit:** `generate`, `migrate`, `push`, `pull`, `check`, `drop`, `typegen`.
48
+ - **Studio:** browse and edit data in your browser (`mizzle studio`).
49
+ - **Safe by default:** values are always bind parameters, `update`/`delete` refuse to run
50
+ without `.where()`, migrations are locked, transactional and tamper-checked, and errors
51
+ never include row values.
52
+
53
+ Built for PostgreSQL first; other SQL databases plug in through the dialect layer
54
+ (`src/mizzle/dialects/`).
55
+
56
+ A complete example app (FastAPI, migrations, seed data, tests) lives in
57
+ [`examples/social`](examples/social).
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pip install mizzle # ORM + migration CLI
63
+ pip install "mizzle[studio]" # + Studio (its UI downloads into your venv on first run, ~250 KB)
64
+ ```
65
+
66
+ ## Schema
67
+
68
+ ```python
69
+ # app/db/schema.py
70
+ import enum
71
+ from typing import final, override
72
+
73
+ from mizzle import Table, index, many, one, sql
74
+ from mizzle.pg import integer, pg_enum, serial, text, timestamp, varchar
75
+
76
+
77
+ class Role(enum.StrEnum):
78
+ ADMIN = "admin"
79
+ USER = "user"
80
+
81
+
82
+ @final
83
+ class Users(Table, name="users"):
84
+ id = serial().primary_key() # Column[int]
85
+ email = varchar(255).not_null().unique() # Column[str]
86
+ bio = text() # Column[str | None]
87
+ role = pg_enum(Role).not_null().default(Role.USER) # Column[Role]
88
+ created_at = timestamp(tz=True).not_null().default(sql("now()"))
89
+
90
+ posts = many(lambda: Posts)
91
+
92
+
93
+ @final
94
+ class Posts(Table, name="posts"):
95
+ id = serial().primary_key()
96
+ author_id = integer().not_null().references(lambda: Users.id, on_delete="cascade")
97
+ title = text().not_null()
98
+
99
+ author = one(lambda: Users, fields=lambda: [Posts.author_id], references=lambda: [Users.id])
100
+
101
+ @classmethod
102
+ @override
103
+ def config(cls):
104
+ return [index("posts_author_idx").on(cls.author_id)]
105
+ ```
106
+
107
+ ## Queries
108
+
109
+ ```python
110
+ from mizzle import Database, desc, eq
111
+
112
+ async with await Database.connect("postgresql://...", schema=[Users, Posts]) as db:
113
+ [ann] = await db.insert(Users).values(email="ann@x.io").returning() # Users
114
+ admins = await db.select().from_(Users).where(eq(Users.role, Role.ADMIN)) # list[Users]
115
+ pairs = await (
116
+ db.select(Users, Posts).from_(Users).left_join(Posts, eq(Posts.author_id, Users.id))
117
+ ) # list[tuple[Users, Posts | None]]
118
+
119
+ # Relational: one SQL statement, typed nested results.
120
+ users = await db.query(Users).find_many(
121
+ with_={"posts": {"order_by": desc(Posts.id), "limit": 5}},
122
+ )
123
+ users[0].posts[0].title # str
124
+
125
+ async with db.transaction() as tx:
126
+ await tx.update(Users).set(bio="hi").where(eq(Users.id, ann.id))
127
+ ```
128
+
129
+ Raw SQL: `sql(t"select * from users where id = {user_id}")` (Python 3.14+) or
130
+ `sql("... where id = {}", user_id)`. Values become bind parameters. **Never build the text
131
+ passed to `sql()` from user input** (for example with an f-string).
132
+
133
+ ## Migrations
134
+
135
+ `mizzle init` creates `mizzle.toml`, a starter schema module and the migrations folder
136
+ (`--pyproject` puts the settings in `[tool.mizzle]` of `pyproject.toml` instead):
137
+
138
+ ```toml
139
+ schema = "app.db.schema" # module(s) with your tables
140
+ out = "migrations" # commit this folder
141
+ url_env = "DATABASE_URL" # the URL is read from the environment
142
+ ```
143
+
144
+ ```bash
145
+ mizzle init # set up mizzle.toml, db/schema.py and migrations/
146
+ mizzle generate # write migrations/0001_.../migration.sql + snapshot.json
147
+ mizzle migrate # apply pending migrations (safe with many app servers)
148
+ mizzle push # dev: sync schema straight to the database
149
+ mizzle pull # existing database -> your schema module (or pulled_schema.py next to it)
150
+ mizzle check # find broken or clashing migrations
151
+ mizzle drop # delete the latest, unapplied migration
152
+ mizzle typegen # TypedDicts for insert/update values
153
+ mizzle studio # open Studio
154
+ ```
155
+
156
+ Renames: on a terminal, `generate` asks whether a new column is a rename. In CI pass
157
+ `--rename users.name=full_name`. Changes that can lose data need confirmation or
158
+ `--allow-data-loss`. From code: `await mizzle.kit.migrate(db, "migrations")`.
159
+
160
+ ## Development
161
+
162
+ ```bash
163
+ docker compose up -d
164
+ export MIZZLE_TEST_URL=postgresql://mizzle:mizzle@localhost:54329/mizzle
165
+ uv run pytest # integration tests skip when MIZZLE_TEST_URL is unset
166
+ uv run pyright && uv run mypy
167
+ uv run ruff check && uv run ruff format --check
168
+
169
+ cd studio-ui && pnpm install && pnpm build # builds the UI into src/mizzle/studio/static
170
+ pnpm dev # UI dev server; proxies /api to `mizzle studio`
171
+ pnpm check # Biome: format, lint, sort imports (--write)
172
+ ```
173
+
174
+ Releases: push a `v*` tag. `.github/workflows/release.yml` builds the UI, packages it into
175
+ the wheel, and publishes to PyPI with trusted publishing.
mizzle-0.1.0/README.md ADDED
@@ -0,0 +1,146 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
4
+ <img alt="mizzle" src="assets/logo.svg" width="320">
5
+ </picture>
6
+ </p>
7
+
8
+ # mizzle
9
+
10
+ A fully typed, async ORM for Python and PostgreSQL: write queries that read like SQL,
11
+ get typed rows back, and evolve your schema with generated migrations. If you know
12
+ [Drizzle](https://orm.drizzle.team) from TypeScript, mizzle will feel familiar.
13
+
14
+ - **Schema in Python classes.** Result rows are typed for pyright and mypy, with no code
15
+ generation.
16
+ - **Two query styles:** a SQL-like builder (`select`, `insert`, `update`, `delete`, joins,
17
+ CTEs, upserts) and relational queries that load nested data in one SQL statement.
18
+ - **Migration kit:** `generate`, `migrate`, `push`, `pull`, `check`, `drop`, `typegen`.
19
+ - **Studio:** browse and edit data in your browser (`mizzle studio`).
20
+ - **Safe by default:** values are always bind parameters, `update`/`delete` refuse to run
21
+ without `.where()`, migrations are locked, transactional and tamper-checked, and errors
22
+ never include row values.
23
+
24
+ Built for PostgreSQL first; other SQL databases plug in through the dialect layer
25
+ (`src/mizzle/dialects/`).
26
+
27
+ A complete example app (FastAPI, migrations, seed data, tests) lives in
28
+ [`examples/social`](examples/social).
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install mizzle # ORM + migration CLI
34
+ pip install "mizzle[studio]" # + Studio (its UI downloads into your venv on first run, ~250 KB)
35
+ ```
36
+
37
+ ## Schema
38
+
39
+ ```python
40
+ # app/db/schema.py
41
+ import enum
42
+ from typing import final, override
43
+
44
+ from mizzle import Table, index, many, one, sql
45
+ from mizzle.pg import integer, pg_enum, serial, text, timestamp, varchar
46
+
47
+
48
+ class Role(enum.StrEnum):
49
+ ADMIN = "admin"
50
+ USER = "user"
51
+
52
+
53
+ @final
54
+ class Users(Table, name="users"):
55
+ id = serial().primary_key() # Column[int]
56
+ email = varchar(255).not_null().unique() # Column[str]
57
+ bio = text() # Column[str | None]
58
+ role = pg_enum(Role).not_null().default(Role.USER) # Column[Role]
59
+ created_at = timestamp(tz=True).not_null().default(sql("now()"))
60
+
61
+ posts = many(lambda: Posts)
62
+
63
+
64
+ @final
65
+ class Posts(Table, name="posts"):
66
+ id = serial().primary_key()
67
+ author_id = integer().not_null().references(lambda: Users.id, on_delete="cascade")
68
+ title = text().not_null()
69
+
70
+ author = one(lambda: Users, fields=lambda: [Posts.author_id], references=lambda: [Users.id])
71
+
72
+ @classmethod
73
+ @override
74
+ def config(cls):
75
+ return [index("posts_author_idx").on(cls.author_id)]
76
+ ```
77
+
78
+ ## Queries
79
+
80
+ ```python
81
+ from mizzle import Database, desc, eq
82
+
83
+ async with await Database.connect("postgresql://...", schema=[Users, Posts]) as db:
84
+ [ann] = await db.insert(Users).values(email="ann@x.io").returning() # Users
85
+ admins = await db.select().from_(Users).where(eq(Users.role, Role.ADMIN)) # list[Users]
86
+ pairs = await (
87
+ db.select(Users, Posts).from_(Users).left_join(Posts, eq(Posts.author_id, Users.id))
88
+ ) # list[tuple[Users, Posts | None]]
89
+
90
+ # Relational: one SQL statement, typed nested results.
91
+ users = await db.query(Users).find_many(
92
+ with_={"posts": {"order_by": desc(Posts.id), "limit": 5}},
93
+ )
94
+ users[0].posts[0].title # str
95
+
96
+ async with db.transaction() as tx:
97
+ await tx.update(Users).set(bio="hi").where(eq(Users.id, ann.id))
98
+ ```
99
+
100
+ Raw SQL: `sql(t"select * from users where id = {user_id}")` (Python 3.14+) or
101
+ `sql("... where id = {}", user_id)`. Values become bind parameters. **Never build the text
102
+ passed to `sql()` from user input** (for example with an f-string).
103
+
104
+ ## Migrations
105
+
106
+ `mizzle init` creates `mizzle.toml`, a starter schema module and the migrations folder
107
+ (`--pyproject` puts the settings in `[tool.mizzle]` of `pyproject.toml` instead):
108
+
109
+ ```toml
110
+ schema = "app.db.schema" # module(s) with your tables
111
+ out = "migrations" # commit this folder
112
+ url_env = "DATABASE_URL" # the URL is read from the environment
113
+ ```
114
+
115
+ ```bash
116
+ mizzle init # set up mizzle.toml, db/schema.py and migrations/
117
+ mizzle generate # write migrations/0001_.../migration.sql + snapshot.json
118
+ mizzle migrate # apply pending migrations (safe with many app servers)
119
+ mizzle push # dev: sync schema straight to the database
120
+ mizzle pull # existing database -> your schema module (or pulled_schema.py next to it)
121
+ mizzle check # find broken or clashing migrations
122
+ mizzle drop # delete the latest, unapplied migration
123
+ mizzle typegen # TypedDicts for insert/update values
124
+ mizzle studio # open Studio
125
+ ```
126
+
127
+ Renames: on a terminal, `generate` asks whether a new column is a rename. In CI pass
128
+ `--rename users.name=full_name`. Changes that can lose data need confirmation or
129
+ `--allow-data-loss`. From code: `await mizzle.kit.migrate(db, "migrations")`.
130
+
131
+ ## Development
132
+
133
+ ```bash
134
+ docker compose up -d
135
+ export MIZZLE_TEST_URL=postgresql://mizzle:mizzle@localhost:54329/mizzle
136
+ uv run pytest # integration tests skip when MIZZLE_TEST_URL is unset
137
+ uv run pyright && uv run mypy
138
+ uv run ruff check && uv run ruff format --check
139
+
140
+ cd studio-ui && pnpm install && pnpm build # builds the UI into src/mizzle/studio/static
141
+ pnpm dev # UI dev server; proxies /api to `mizzle studio`
142
+ pnpm check # Biome: format, lint, sort imports (--write)
143
+ ```
144
+
145
+ Releases: push a `v*` tag. `.github/workflows/release.yml` builds the UI, packages it into
146
+ the wheel, and publishes to PyPI with trusted publishing.
Binary file
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#0B1020"/><g stroke-width="5" stroke-linecap="round" transform="translate(6.4 6.4) scale(0.8)"><path d="M14.0 20.0H50.0" stroke="#93B4FF"/><path d="M22.0 32.0H54.0" stroke="#5F86EE"/><path d="M10.0 44.0H38.0" stroke="#3A5BB8"/></g></svg>
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 186 64" role="img" aria-label="mizzle"><g stroke-width="6" stroke-linecap="round"><path d="M12 20.0H48" stroke="#93B4FF"/><path d="M20 32.0H52" stroke="#5F86EE"/><path d="M8 44.0H36" stroke="#3A5BB8"/></g><!-- "mizzle" set in Geist (SIL Open Font License), converted to outlines --><path transform="translate(64 43)" fill="#F8FAFC" d="M2.584 0.0V-20.33H7.353L7.524 -15.389999999999999L7.03 -15.618Q7.41 -17.252 8.265 -18.4015Q9.12 -19.551 10.3455 -20.168499999999998Q11.571 -20.785999999999998 13.033999999999999 -20.785999999999998Q15.599 -20.785999999999998 17.185499999999998 -19.313499999999998Q18.772 -17.841 19.209 -15.314L18.525 -15.295Q18.867 -17.081 19.7125 -18.296999999999997Q20.558 -19.512999999999998 21.8215 -20.149499999999996Q23.085 -20.785999999999998 24.662 -20.785999999999998Q26.771 -20.785999999999998 28.291 -19.902499999999996Q29.811 -19.019 30.6375 -17.2995Q31.464 -15.58 31.464 -13.072V0.0H26.182V-11.647Q26.182 -14.097999999999999 25.3935 -15.3235Q24.605 -16.549 22.913999999999998 -16.549Q21.812 -16.549 21.033 -15.96Q20.253999999999998 -15.371 19.836 -14.25Q19.418 -13.129 19.418 -11.533V0.0H14.591999999999999V-11.533Q14.591999999999999 -13.965 13.87 -15.257Q13.148 -16.549 11.381 -16.549Q10.279 -16.549 9.5 -15.96Q8.721 -15.371 8.2935 -14.2405Q7.866 -13.11 7.866 -11.533V0.0ZM35.872 0.0V-20.33H41.153999999999996V0.0ZM35.777 -22.933V-27.378999999999998H41.248999999999995V-22.933ZM44.878 0.0V-4.256L56.354 -16.320999999999998H45.334V-20.33H62.643V-16.073999999999998L51.015 -4.009H62.928V0.0ZM65.968 0.0V-4.256L77.444 -16.320999999999998H66.42399999999999V-20.33H83.733V-16.073999999999998L72.10499999999999 -4.009H84.018V0.0ZM92.834 0.0Q90.47800000000001 0.0 89.11000000000001 -1.197Q87.742 -2.394 87.742 -5.054V-26.98H93.024V-5.6049999999999995Q93.024 -4.769 93.4325 -4.389Q93.84100000000001 -4.009 94.601 -4.009H96.026V0.0ZM107.53999999999999 0.45599999999999996Q104.462 0.45599999999999996 102.19149999999999 -0.855Q99.92099999999999 -2.166 98.6765 -4.56Q97.432 -6.954 97.432 -10.165Q97.432 -13.376 98.6765 -15.751000000000001Q99.92099999999999 -18.126 102.1725 -19.456Q104.424 -20.785999999999998 107.464 -20.785999999999998Q110.428 -20.785999999999998 112.64150000000001 -19.474999999999998Q114.855 -18.163999999999998 116.0615 -15.713Q117.268 -13.262 117.268 -9.842V-8.721H102.904Q103.037 -6.175 104.291 -4.9115Q105.545 -3.6479999999999997 107.616 -3.6479999999999997Q109.155 -3.6479999999999997 110.181 -4.3225Q111.207 -4.997 111.625 -6.289L116.983 -5.966Q116.166 -2.964 113.6485 -1.254Q111.131 0.45599999999999996 107.53999999999999 0.45599999999999996ZM102.904 -12.103H111.834Q111.701 -14.459 110.5135 -15.6085Q109.326 -16.758 107.464 -16.758Q105.602 -16.758 104.386 -15.551499999999999Q103.17 -14.344999999999999 102.904 -12.103Z"/></svg>
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 186 64" role="img" aria-label="mizzle"><g stroke-width="6" stroke-linecap="round"><path d="M12 20.0H48" stroke="#1D4ED8"/><path d="M20 32.0H52" stroke="#5B7FE3"/><path d="M8 44.0H36" stroke="#A3B8F1"/></g><!-- "mizzle" set in Geist (SIL Open Font License), converted to outlines --><path transform="translate(64 43)" fill="#0F172A" d="M2.584 0.0V-20.33H7.353L7.524 -15.389999999999999L7.03 -15.618Q7.41 -17.252 8.265 -18.4015Q9.12 -19.551 10.3455 -20.168499999999998Q11.571 -20.785999999999998 13.033999999999999 -20.785999999999998Q15.599 -20.785999999999998 17.185499999999998 -19.313499999999998Q18.772 -17.841 19.209 -15.314L18.525 -15.295Q18.867 -17.081 19.7125 -18.296999999999997Q20.558 -19.512999999999998 21.8215 -20.149499999999996Q23.085 -20.785999999999998 24.662 -20.785999999999998Q26.771 -20.785999999999998 28.291 -19.902499999999996Q29.811 -19.019 30.6375 -17.2995Q31.464 -15.58 31.464 -13.072V0.0H26.182V-11.647Q26.182 -14.097999999999999 25.3935 -15.3235Q24.605 -16.549 22.913999999999998 -16.549Q21.812 -16.549 21.033 -15.96Q20.253999999999998 -15.371 19.836 -14.25Q19.418 -13.129 19.418 -11.533V0.0H14.591999999999999V-11.533Q14.591999999999999 -13.965 13.87 -15.257Q13.148 -16.549 11.381 -16.549Q10.279 -16.549 9.5 -15.96Q8.721 -15.371 8.2935 -14.2405Q7.866 -13.11 7.866 -11.533V0.0ZM35.872 0.0V-20.33H41.153999999999996V0.0ZM35.777 -22.933V-27.378999999999998H41.248999999999995V-22.933ZM44.878 0.0V-4.256L56.354 -16.320999999999998H45.334V-20.33H62.643V-16.073999999999998L51.015 -4.009H62.928V0.0ZM65.968 0.0V-4.256L77.444 -16.320999999999998H66.42399999999999V-20.33H83.733V-16.073999999999998L72.10499999999999 -4.009H84.018V0.0ZM92.834 0.0Q90.47800000000001 0.0 89.11000000000001 -1.197Q87.742 -2.394 87.742 -5.054V-26.98H93.024V-5.6049999999999995Q93.024 -4.769 93.4325 -4.389Q93.84100000000001 -4.009 94.601 -4.009H96.026V0.0ZM107.53999999999999 0.45599999999999996Q104.462 0.45599999999999996 102.19149999999999 -0.855Q99.92099999999999 -2.166 98.6765 -4.56Q97.432 -6.954 97.432 -10.165Q97.432 -13.376 98.6765 -15.751000000000001Q99.92099999999999 -18.126 102.1725 -19.456Q104.424 -20.785999999999998 107.464 -20.785999999999998Q110.428 -20.785999999999998 112.64150000000001 -19.474999999999998Q114.855 -18.163999999999998 116.0615 -15.713Q117.268 -13.262 117.268 -9.842V-8.721H102.904Q103.037 -6.175 104.291 -4.9115Q105.545 -3.6479999999999997 107.616 -3.6479999999999997Q109.155 -3.6479999999999997 110.181 -4.3225Q111.207 -4.997 111.625 -6.289L116.983 -5.966Q116.166 -2.964 113.6485 -1.254Q111.131 0.45599999999999996 107.53999999999999 0.45599999999999996ZM102.904 -12.103H111.834Q111.701 -14.459 110.5135 -15.6085Q109.326 -16.758 107.464 -16.758Q105.602 -16.758 104.386 -15.551499999999999Q103.17 -14.344999999999999 102.904 -12.103Z"/></svg>
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="mizzle"><g stroke-width="6" stroke-linecap="round"><path d="M14.0 20.0H50.0" stroke="#93B4FF"/><path d="M22.0 32.0H54.0" stroke="#5F86EE"/><path d="M10.0 44.0H38.0" stroke="#3A5BB8"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="mizzle"><g stroke-width="6" stroke-linecap="round"><path d="M14.0 20.0H50.0" stroke="#1D4ED8"/><path d="M22.0 32.0H54.0" stroke="#5B7FE3"/><path d="M10.0 44.0H38.0" stroke="#A3B8F1"/></g></svg>
Binary file
@@ -0,0 +1,4 @@
1
+ # Copy to .env. The Docker Postgres from the repo's docker-compose.yml:
2
+ DATABASE_URL=postgresql://mizzle:mizzle@localhost:54329/mizzle_dev
3
+ # Apply pending migrations when the API starts (safe with many processes).
4
+ MIGRATE_ON_START=1
@@ -0,0 +1,49 @@
1
+ # mizzle example: a small social network API
2
+
3
+ FastAPI + mizzle: profiles, follows, posts with visibility (public / followers / private),
4
+ comments, likes and bookmarks. It shows the patterns a real app needs:
5
+
6
+ - **Schema** in `social/schema.py` (`@final` tables, enums, composite keys, checks, indexes,
7
+ relations) and its **migration history** in `migrations/`.
8
+ - **Request models with allowlists** (`social/models.py`): clients can only send the listed
9
+ fields, so they cannot set ids, authors or timestamps.
10
+ - **Relational queries** for nested data (a post with its comments and their authors) and
11
+ typed `select` queries for counts.
12
+ - **Upserts** for idempotent likes, follows and bookmarks.
13
+ - **Migrations at startup** (`MIGRATE_ON_START=1`), safe with many workers (advisory lock).
14
+ - **Database errors as HTTP**: unique → 409, check constraint → 422, missing reference → 404.
15
+ - **End-to-end tests** against a fresh database (`tests/test_api.py`).
16
+
17
+ ## Run it
18
+
19
+ ```bash
20
+ docker compose -f ../../docker-compose.yml up -d # Postgres on localhost:54329
21
+ cp .env.example .env
22
+ uv sync
23
+ uv run mizzle migrate # or rely on MIGRATE_ON_START=1
24
+ uv run python -m social.seed # optional demo data
25
+ uv run uvicorn social.app:create_app --factory --reload
26
+ ```
27
+
28
+ Open http://127.0.0.1:8000/docs. Authentication is out of scope: send `X-User-Id: <id>`
29
+ to act as a user (the seed creates ids 1-24).
30
+
31
+ ```bash
32
+ curl -s localhost:8000/feed -H 'X-User-Id: 1' | python -m json.tool | head -40
33
+ uv run mizzle studio # browse the data
34
+ ```
35
+
36
+ ## Change the schema
37
+
38
+ Edit `social/schema.py`, then:
39
+
40
+ ```bash
41
+ uv run mizzle generate --name what_changed
42
+ uv run mizzle migrate
43
+ ```
44
+
45
+ ## Test
46
+
47
+ ```bash
48
+ MIZZLE_TEST_URL=postgresql://mizzle:mizzle@localhost:54329/mizzle uv run pytest
49
+ ```
@@ -0,0 +1,88 @@
1
+ -- create enum visibility
2
+ -- create table users
3
+ -- create table follows
4
+ -- create table posts
5
+ -- create table comments
6
+ -- create table likes
7
+ -- create index follows_followee_idx
8
+ -- create index posts_author_created_idx
9
+ -- create index comments_post_idx
10
+ -- create index likes_post_idx
11
+ -- add foreign key follows_follower_id_fkey
12
+ -- add foreign key follows_followee_id_fkey
13
+ -- add foreign key posts_author_id_fkey
14
+ -- add foreign key comments_post_id_fkey
15
+ -- add foreign key comments_author_id_fkey
16
+ -- add foreign key likes_user_id_fkey
17
+ -- add foreign key likes_post_id_fkey
18
+ CREATE TYPE "visibility" AS ENUM ('public', 'followers', 'private');
19
+ -- mizzle:breakpoint
20
+ CREATE TABLE "users" (
21
+ "id" serial NOT NULL,
22
+ "username" character varying(32) NOT NULL,
23
+ "email" character varying(255) NOT NULL,
24
+ "display_name" text NOT NULL,
25
+ "bio" text,
26
+ "created_at" timestamp with time zone NOT NULL DEFAULT now(),
27
+ CONSTRAINT "users_pkey" PRIMARY KEY ("id"),
28
+ CONSTRAINT "users_username_key" UNIQUE ("username"),
29
+ CONSTRAINT "users_email_key" UNIQUE ("email"),
30
+ CONSTRAINT "users_username_format" CHECK (username ~ '^[a-z0-9_]{3,32}$')
31
+ );
32
+ -- mizzle:breakpoint
33
+ CREATE TABLE "follows" (
34
+ "follower_id" integer NOT NULL,
35
+ "followee_id" integer NOT NULL,
36
+ "created_at" timestamp with time zone NOT NULL DEFAULT now(),
37
+ CONSTRAINT "follows_pkey" PRIMARY KEY ("follower_id", "followee_id"),
38
+ CONSTRAINT "follows_not_self" CHECK (follower_id <> followee_id)
39
+ );
40
+ -- mizzle:breakpoint
41
+ CREATE TABLE "posts" (
42
+ "id" serial NOT NULL,
43
+ "author_id" integer NOT NULL,
44
+ "body" text NOT NULL,
45
+ "visibility" visibility NOT NULL DEFAULT 'public',
46
+ "meta" jsonb,
47
+ "created_at" timestamp with time zone NOT NULL DEFAULT now(),
48
+ CONSTRAINT "posts_pkey" PRIMARY KEY ("id"),
49
+ CONSTRAINT "posts_body_not_empty" CHECK (length(body) > 0)
50
+ );
51
+ -- mizzle:breakpoint
52
+ CREATE TABLE "comments" (
53
+ "id" serial NOT NULL,
54
+ "post_id" integer NOT NULL,
55
+ "author_id" integer NOT NULL,
56
+ "body" text NOT NULL,
57
+ "created_at" timestamp with time zone NOT NULL DEFAULT now(),
58
+ CONSTRAINT "comments_pkey" PRIMARY KEY ("id")
59
+ );
60
+ -- mizzle:breakpoint
61
+ CREATE TABLE "likes" (
62
+ "user_id" integer NOT NULL,
63
+ "post_id" integer NOT NULL,
64
+ "created_at" timestamp with time zone NOT NULL DEFAULT now(),
65
+ CONSTRAINT "likes_pkey" PRIMARY KEY ("user_id", "post_id")
66
+ );
67
+ -- mizzle:breakpoint
68
+ CREATE INDEX "follows_followee_idx" ON "follows" ("followee_id");
69
+ -- mizzle:breakpoint
70
+ CREATE INDEX "posts_author_created_idx" ON "posts" ("author_id", "created_at");
71
+ -- mizzle:breakpoint
72
+ CREATE INDEX "comments_post_idx" ON "comments" ("post_id");
73
+ -- mizzle:breakpoint
74
+ CREATE INDEX "likes_post_idx" ON "likes" ("post_id");
75
+ -- mizzle:breakpoint
76
+ ALTER TABLE "follows" ADD CONSTRAINT "follows_follower_id_fkey" FOREIGN KEY ("follower_id") REFERENCES "users" ("id") ON DELETE CASCADE;
77
+ -- mizzle:breakpoint
78
+ ALTER TABLE "follows" ADD CONSTRAINT "follows_followee_id_fkey" FOREIGN KEY ("followee_id") REFERENCES "users" ("id") ON DELETE CASCADE;
79
+ -- mizzle:breakpoint
80
+ ALTER TABLE "posts" ADD CONSTRAINT "posts_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "users" ("id") ON DELETE CASCADE;
81
+ -- mizzle:breakpoint
82
+ ALTER TABLE "comments" ADD CONSTRAINT "comments_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "posts" ("id") ON DELETE CASCADE;
83
+ -- mizzle:breakpoint
84
+ ALTER TABLE "comments" ADD CONSTRAINT "comments_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "users" ("id") ON DELETE CASCADE;
85
+ -- mizzle:breakpoint
86
+ ALTER TABLE "likes" ADD CONSTRAINT "likes_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE;
87
+ -- mizzle:breakpoint
88
+ ALTER TABLE "likes" ADD CONSTRAINT "likes_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "posts" ("id") ON DELETE CASCADE;