alterdb 0.2.2__tar.gz → 0.2.3__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 (106) hide show
  1. {alterdb-0.2.2 → alterdb-0.2.3}/CHANGELOG.md +59 -0
  2. {alterdb-0.2.2 → alterdb-0.2.3}/PKG-INFO +1 -1
  3. {alterdb-0.2.2 → alterdb-0.2.3}/pyproject.toml +1 -1
  4. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/generators/_surgical.py +37 -6
  5. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/mcp_server.py +16 -2
  6. {alterdb-0.2.2 → alterdb-0.2.3}/.env.example +0 -0
  7. {alterdb-0.2.2 → alterdb-0.2.3}/.gitignore +0 -0
  8. {alterdb-0.2.2 → alterdb-0.2.3}/.python-version +0 -0
  9. {alterdb-0.2.2 → alterdb-0.2.3}/LICENSE +0 -0
  10. {alterdb-0.2.2 → alterdb-0.2.3}/README.md +0 -0
  11. {alterdb-0.2.2 → alterdb-0.2.3}/docker-compose.yml +0 -0
  12. {alterdb-0.2.2 → alterdb-0.2.3}/docs/Canvas.png +0 -0
  13. {alterdb-0.2.2 → alterdb-0.2.3}/examples/saas-starter/alembic/alembic.ini +0 -0
  14. {alterdb-0.2.2 → alterdb-0.2.3}/examples/saas-starter/alembic/env.py +0 -0
  15. {alterdb-0.2.2 → alterdb-0.2.3}/examples/saas-starter/alembic/script.py.mako +0 -0
  16. {alterdb-0.2.2 → alterdb-0.2.3}/examples/saas-starter/app/__init__.py +0 -0
  17. {alterdb-0.2.2 → alterdb-0.2.3}/examples/saas-starter/app/database.py +0 -0
  18. {alterdb-0.2.2 → alterdb-0.2.3}/examples/saas-starter/app/enums.py +0 -0
  19. {alterdb-0.2.2 → alterdb-0.2.3}/examples/saas-starter/app/main.py +0 -0
  20. {alterdb-0.2.2 → alterdb-0.2.3}/examples/saas-starter/app/models/parents.py +0 -0
  21. {alterdb-0.2.2 → alterdb-0.2.3}/examples/saas-starter/app/models/starter.py +0 -0
  22. {alterdb-0.2.2 → alterdb-0.2.3}/examples/saas-starter/pyproject.toml +0 -0
  23. {alterdb-0.2.2 → alterdb-0.2.3}/examples/saas-starter/tests/__init__.py +0 -0
  24. {alterdb-0.2.2 → alterdb-0.2.3}/examples/saas-starter/tests/test_integration.py +0 -0
  25. {alterdb-0.2.2 → alterdb-0.2.3}/examples/saas-starter/tests/test_round_trip.py +0 -0
  26. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/__init__.py +0 -0
  27. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/canvas/__init__.py +0 -0
  28. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/canvas/server.py +0 -0
  29. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/canvas/static/canvas.js +0 -0
  30. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/canvas/static/index.html +0 -0
  31. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/canvas/static/style.css +0 -0
  32. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/cli.py +0 -0
  33. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/data/demo_schema.alter +0 -0
  34. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/diff.py +0 -0
  35. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/diff_format.py +0 -0
  36. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/errors.py +0 -0
  37. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/exporters/__init__.py +0 -0
  38. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/exporters/alter_file.py +0 -0
  39. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/exporters/mermaid.py +0 -0
  40. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/exporters/sql.py +0 -0
  41. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/generators/__init__.py +0 -0
  42. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/generators/base.py +0 -0
  43. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/generators/sqlalchemy.py +0 -0
  44. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/generators/sqlmodel.py +0 -0
  45. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/importers/__init__.py +0 -0
  46. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/importers/alter_file.py +0 -0
  47. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/importers/database.py +0 -0
  48. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/importers/sql.py +0 -0
  49. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/layout.py +0 -0
  50. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/merge_driver.py +0 -0
  51. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/parsers/__init__.py +0 -0
  52. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/parsers/base.py +0 -0
  53. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/parsers/sqlalchemy.py +0 -0
  54. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/parsers/sqlmodel.py +0 -0
  55. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/schema.py +0 -0
  56. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/staging.py +0 -0
  57. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/templates/auth.alter +0 -0
  58. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/templates/cms.alter +0 -0
  59. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/templates/ecommerce.alter +0 -0
  60. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/templates/saas-base.alter +0 -0
  61. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/types.py +0 -0
  62. {alterdb-0.2.2 → alterdb-0.2.3}/src/alter/validate.py +0 -0
  63. {alterdb-0.2.2 → alterdb-0.2.3}/src/alterdb/__init__.py +0 -0
  64. {alterdb-0.2.2 → alterdb-0.2.3}/templates/auth.alter +0 -0
  65. {alterdb-0.2.2 → alterdb-0.2.3}/templates/cms.alter +0 -0
  66. {alterdb-0.2.2 → alterdb-0.2.3}/templates/ecommerce.alter +0 -0
  67. {alterdb-0.2.2 → alterdb-0.2.3}/templates/saas-base.alter +0 -0
  68. {alterdb-0.2.2 → alterdb-0.2.3}/tests/__init__.py +0 -0
  69. {alterdb-0.2.2 → alterdb-0.2.3}/tests/conftest.py +0 -0
  70. {alterdb-0.2.2 → alterdb-0.2.3}/tests/fixtures/__init__.py +0 -0
  71. {alterdb-0.2.2 → alterdb-0.2.3}/tests/fixtures/sqlalchemy_models.py +0 -0
  72. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_alembic_wrapper.py +0 -0
  73. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_bug10_sa_column_type.py +0 -0
  74. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_bug17_apply_preserve.py +0 -0
  75. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_bug6_list_any_json.py +0 -0
  76. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_bug7_table_args.py +0 -0
  77. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_bug7_unreferenced_enums.py +0 -0
  78. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_bug8_surgical_preserve.py +0 -0
  79. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_bug_table_args_tuple.py +0 -0
  80. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_bugs_v013.py +0 -0
  81. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_canvas_actions.py +0 -0
  82. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_canvas_cors.py +0 -0
  83. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_cli.py +0 -0
  84. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_diff.py +0 -0
  85. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_e2e.py +0 -0
  86. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_enum_routing.py +0 -0
  87. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_exporters.py +0 -0
  88. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_generator_sqlalchemy.py +0 -0
  89. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_generator_sqlmodel.py +0 -0
  90. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_importer_database.py +0 -0
  91. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_importers.py +0 -0
  92. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_layout.py +0 -0
  93. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_mcp_server.py +0 -0
  94. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_merge_driver.py +0 -0
  95. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_migration_sql.py +0 -0
  96. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_parser_sqlalchemy.py +0 -0
  97. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_parser_sqlmodel.py +0 -0
  98. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_round_trip.py +0 -0
  99. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_schema.py +0 -0
  100. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_smoke.py +0 -0
  101. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_staging.py +0 -0
  102. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_surgical.py +0 -0
  103. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_sync_from_code.py +0 -0
  104. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_types.py +0 -0
  105. {alterdb-0.2.2 → alterdb-0.2.3}/tests/test_validate.py +0 -0
  106. {alterdb-0.2.2 → alterdb-0.2.3}/uv.lock +0 -0
@@ -2,6 +2,65 @@
2
2
 
3
3
  All notable changes to Alter are documented here.
4
4
 
5
+ ## [0.2.3] — 2026-03-15
6
+
7
+ ### Bug Fixes
8
+
9
+ #### `alter mcp` crashes with a cryptic `ModuleNotFoundError` when `mcp < 1.2.0` (Bug A)
10
+
11
+ `alter mcp` calls `init_mcp()` which imports `FastMCP` from `mcp.server.fastmcp`. That
12
+ submodule was introduced in `mcp 1.2.0`; older installations raise a bare
13
+ `ModuleNotFoundError: No module named 'mcp.server.fastmcp'` with no indication of how
14
+ to fix it.
15
+
16
+ `init_mcp()` now wraps the import in a `try/except ImportError` and raises an
17
+ `AlterError` with an actionable message:
18
+
19
+ ```
20
+ 'alter mcp' requires mcp>=1.2.0, but mcp==1.1.3 is installed.
21
+ Upgrade with: pip install 'mcp>=1.2.0'
22
+ ```
23
+
24
+ The error surfaces cleanly in the CLI (no "MCP server error:" prefix) because the CLI
25
+ already handles `AlterError` separately from generic exceptions.
26
+
27
+ A second guard wraps the cosmetic `_mcp_server.version` assignment in
28
+ `try/except AttributeError` so that future changes to `mcp` internals do not break
29
+ `alter mcp` startup.
30
+
31
+ #### `alter apply` spuriously rewrites `datetime.now(timezone.utc)` defaults on Python 3.11+ (Bug B)
32
+
33
+ When a model file contained a column with `default_factory=lambda: datetime.now(timezone.utc)`,
34
+ running `alter apply` on Python 3.11+ would rewrite the line even though nothing had
35
+ changed in the schema.
36
+
37
+ Root cause: `_parse_field_kwargs` normalises kwargs via `ast.unparse`, and the Python
38
+ `ast` module changed how it serialises zero-argument lambdas between versions —
39
+ Python ≤ 3.10 produces `"lambda :"` (with a space after `lambda`), while Python 3.11+
40
+ produces `"lambda:"` (no space). Because `ast.unparse` is applied to both the
41
+ existing code and the freshly generated schema line, the comparison reached different
42
+ sides of the `_DEFAULT_FACTORY_EQUIV` lookup depending on the Python version in use,
43
+ causing the surgical patcher to believe a change was needed when there was none.
44
+
45
+ Two-part fix in `generators/_surgical.py`:
46
+
47
+ 1. **`_norm_lambda_ws()` helper** — strips extraneous whitespace between `lambda` and
48
+ `:` for zero-argument lambdas (`re.sub(r"^lambda\s*:", "lambda:", s)`), making
49
+ lambda strings compare equal across Python versions.
50
+
51
+ 2. **Normalization applied consistently** — `_normalize_kw_for_eq` now calls
52
+ `_norm_lambda_ws` on `default_factory` values after the `_DEFAULT_FACTORY_EQUIV`
53
+ lookup, so both the existing-file side and the schema side are normalised before
54
+ comparison. The rebuild path in `_rebuild_field_line` applies the same
55
+ normalization when checking whether the existing value is canonically equivalent.
56
+
57
+ The `_DEFAULT_FACTORY_EQUIV` dict value for `utcnow` was also corrected from
58
+ `"lambda : datetime.now(timezone.utc)"` (with spurious space, matching old Python 3.10
59
+ `ast.unparse` output) to `"lambda: datetime.now(timezone.utc)"` (canonical form) — the
60
+ `_norm_lambda_ws` normalization then makes both forms match on all Python versions.
61
+
62
+ ---
63
+
5
64
  ## [0.2.2] — 2026-03-15
6
65
 
7
66
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alterdb
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Visual schema design for SQLModel and SQLAlchemy. Edit your database as a diagram, write it back as code.
5
5
  Project-URL: Homepage, https://github.com/chimi-labs/alter
6
6
  Project-URL: Repository, https://github.com/chimi-labs/alter
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "alterdb"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "Visual schema design for SQLModel and SQLAlchemy. Edit your database as a diagram, write it back as code."
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE"]
@@ -280,9 +280,15 @@ _DEFAULT_FACTORY_EQUIV: dict[str, str] = {
280
280
  # modern lambda form. Treat both as semantically identical so existing
281
281
  # hand-written code is never touched unless the field actually changes.
282
282
  #
283
- # NOTE: the canonical form uses the exact string produced by ast.unparse(),
284
- # which inserts a space between "lambda" and ":" (i.e. "lambda :").
285
- "datetime.utcnow": "lambda : datetime.now(timezone.utc)",
283
+ # NOTE: ast.unparse() output for zero-argument lambdas varies by version:
284
+ # Python <=3.10 produces "lambda :" (with space before colon);
285
+ # Python 3.11+ produces "lambda:" (no space).
286
+ # _parse_field_kwargs uses ast.unparse on BOTH the existing and generated
287
+ # sides, so the raw strings here may differ by a single space depending on
288
+ # the interpreter. _normalize_kw_for_eq() normalises this via
289
+ # _norm_lambda_ws() after the dict lookup, so the value stored here only
290
+ # needs to be recognisable — the no-space form (3.11 canonical) is used.
291
+ "datetime.utcnow": "lambda: datetime.now(timezone.utc)",
286
292
  # uuid4 (from `from uuid import uuid4`) and uuid.uuid4 (from `import uuid`)
287
293
  # are semantically identical. Treat them as equal so existing hand-written
288
294
  # code using the direct-import form is never touched.
@@ -290,6 +296,22 @@ _DEFAULT_FACTORY_EQUIV: dict[str, str] = {
290
296
  }
291
297
 
292
298
 
299
+ def _norm_lambda_ws(s: str) -> str:
300
+ """Normalise zero-argument lambda whitespace for cross-version compat.
301
+
302
+ ``ast.unparse()`` output for ``lambda: …`` differs between Python versions:
303
+ * Python <=3.10: ``"lambda : expr"`` (space before colon)
304
+ * Python 3.11+: ``"lambda: expr"`` (no space)
305
+
306
+ Both sides of an equality comparison go through ``ast.unparse`` (via
307
+ ``_parse_field_kwargs``), so we must normalise before comparing to avoid
308
+ spurious rewrites depending on which Python is running.
309
+ """
310
+ if s.startswith("lambda"):
311
+ return re.sub(r"^lambda\s*:", "lambda:", s)
312
+ return s
313
+
314
+
293
315
  def _normalize_kw_for_eq(kw: dict[str, str]) -> dict[str, str]:
294
316
  """Normalise mutable-default equivalents to a single canonical form.
295
317
 
@@ -301,6 +323,10 @@ def _normalize_kw_for_eq(kw: dict[str, str]) -> dict[str, str]:
301
323
  ``datetime.utcnow`` → ``lambda: datetime.now(timezone.utc)``) so that
302
324
  fields using the deprecated form are not spuriously rewritten just because
303
325
  the generator produces the modern form.
326
+
327
+ Finally normalises zero-argument lambda whitespace via ``_norm_lambda_ws``
328
+ so that ``"lambda :"`` (Python <=3.10 ast.unparse) and ``"lambda:"``
329
+ (Python 3.11+) compare as equal.
304
330
  """
305
331
  result = dict(kw)
306
332
  for (old_key, old_val), (new_key, new_val) in _MUTABLE_DEFAULT_EQUIV.items():
@@ -309,7 +335,8 @@ def _normalize_kw_for_eq(kw: dict[str, str]) -> dict[str, str]:
309
335
  result[new_key] = new_val
310
336
  if "default_factory" in result:
311
337
  val = result["default_factory"]
312
- result["default_factory"] = _DEFAULT_FACTORY_EQUIV.get(val, val)
338
+ val = _DEFAULT_FACTORY_EQUIV.get(val, val)
339
+ result["default_factory"] = _norm_lambda_ws(val)
313
340
  return result
314
341
 
315
342
 
@@ -458,10 +485,14 @@ def _rebuild_field_line(
458
485
  val = new_kw[key]
459
486
  if key.startswith("__pos_"):
460
487
  merged.append(val) # positional
461
- elif key == "default_factory" and _DEFAULT_FACTORY_EQUIV.get(existing_val) == val:
488
+ elif key == "default_factory" and (
489
+ (_canon := _DEFAULT_FACTORY_EQUIV.get(existing_val)) is not None
490
+ and _norm_lambda_ws(_canon) == _norm_lambda_ws(val)
491
+ ):
462
492
  # The existing value is a known equivalent of what the generator
463
493
  # produces (e.g. datetime.utcnow ↔ lambda: datetime.now(timezone.utc),
464
- # uuid4 ↔ uuid.uuid4).
494
+ # uuid4 ↔ uuid.uuid4). Normalise lambda whitespace on both sides
495
+ # for Python 3.10/3.11 ast.unparse compat.
465
496
  # Preserve the user's original form so we don't introduce noisy diffs.
466
497
  merged.append(f"default_factory={existing_val}")
467
498
  elif existing_val == val and key in existing_raw:
@@ -128,10 +128,24 @@ def init_mcp(alter_file_path: Path) -> None:
128
128
  global _staging, _path
129
129
  _path = alter_file_path
130
130
  _staging = StagingManager(alter_file_path)
131
- from mcp.server.fastmcp import FastMCP # noqa: PLC0415
131
+ try:
132
+ from mcp.server.fastmcp import FastMCP # noqa: PLC0415
133
+ except ImportError:
134
+ import importlib.metadata as _meta # noqa: PLC0415
135
+ try:
136
+ _ver = _meta.version("mcp")
137
+ except _meta.PackageNotFoundError:
138
+ _ver = "unknown"
139
+ raise AlterError(
140
+ f"'alter mcp' requires mcp>=1.2.0, but mcp=={_ver} is installed. "
141
+ f"Upgrade with: pip install 'mcp>=1.2.0'"
142
+ ) from None
132
143
  from alter import __version__ # noqa: PLC0415
133
144
  real_mcp = FastMCP("Alter")
134
- real_mcp._mcp_server.version = __version__
145
+ try:
146
+ real_mcp._mcp_server.version = __version__
147
+ except AttributeError:
148
+ pass # mcp internals changed — version label is cosmetic, not critical
135
149
  mcp._init_real(real_mcp)
136
150
 
137
151
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes