stint 0.1.0a0__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 (89) hide show
  1. stint-0.1.0a0/LICENSE +21 -0
  2. stint-0.1.0a0/PKG-INFO +268 -0
  3. stint-0.1.0a0/README.md +233 -0
  4. stint-0.1.0a0/pyproject.toml +81 -0
  5. stint-0.1.0a0/setup.cfg +4 -0
  6. stint-0.1.0a0/stint/__init__.py +110 -0
  7. stint-0.1.0a0/stint/autogen/__init__.py +25 -0
  8. stint-0.1.0a0/stint/autogen/desired.py +351 -0
  9. stint-0.1.0a0/stint/autogen/diff.py +1001 -0
  10. stint-0.1.0a0/stint/autogen/emit.py +578 -0
  11. stint-0.1.0a0/stint/autogen/loader.py +48 -0
  12. stint-0.1.0a0/stint/autogen/stamp.py +272 -0
  13. stint-0.1.0a0/stint/cli/__init__.py +5 -0
  14. stint-0.1.0a0/stint/cli/app.py +25 -0
  15. stint-0.1.0a0/stint/cli/cmd_reflect.py +123 -0
  16. stint-0.1.0a0/stint/cli/cmd_revision.py +235 -0
  17. stint-0.1.0a0/stint/cli/cmd_stamp.py +89 -0
  18. stint-0.1.0a0/stint/cli/cmd_upgrade.py +165 -0
  19. stint-0.1.0a0/stint/cli/cmd_validate.py +48 -0
  20. stint-0.1.0a0/stint/cli/env_config.py +137 -0
  21. stint-0.1.0a0/stint/cli/main.py +28 -0
  22. stint-0.1.0a0/stint/client/__init__.py +6 -0
  23. stint-0.1.0a0/stint/client/auth.py +67 -0
  24. stint-0.1.0a0/stint/client/http.py +131 -0
  25. stint-0.1.0a0/stint/dialects/__init__.py +8 -0
  26. stint-0.1.0a0/stint/dialects/base.py +33 -0
  27. stint-0.1.0a0/stint/dialects/jira/__init__.py +5 -0
  28. stint-0.1.0a0/stint/dialects/jira/_base.py +883 -0
  29. stint-0.1.0a0/stint/dialects/jira/cloud.py +158 -0
  30. stint-0.1.0a0/stint/dialects/jira/common.py +245 -0
  31. stint-0.1.0a0/stint/engine.py +89 -0
  32. stint-0.1.0a0/stint/exceptions.py +90 -0
  33. stint-0.1.0a0/stint/fields.py +83 -0
  34. stint-0.1.0a0/stint/migrations/__init__.py +40 -0
  35. stint-0.1.0a0/stint/migrations/base.py +148 -0
  36. stint-0.1.0a0/stint/migrations/context.py +52 -0
  37. stint-0.1.0a0/stint/migrations/exceptions.py +24 -0
  38. stint-0.1.0a0/stint/migrations/loader.py +62 -0
  39. stint-0.1.0a0/stint/migrations/op.py +889 -0
  40. stint-0.1.0a0/stint/migrations/runner.py +167 -0
  41. stint-0.1.0a0/stint/py.typed +0 -0
  42. stint-0.1.0a0/stint/query/__init__.py +27 -0
  43. stint-0.1.0a0/stint/query/adf.py +28 -0
  44. stint-0.1.0a0/stint/query/columns.py +47 -0
  45. stint-0.1.0a0/stint/query/expr.py +198 -0
  46. stint-0.1.0a0/stint/query/hydrate.py +146 -0
  47. stint-0.1.0a0/stint/query/payload.py +206 -0
  48. stint-0.1.0a0/stint/query/select.py +64 -0
  49. stint-0.1.0a0/stint/query/session.py +282 -0
  50. stint-0.1.0a0/stint/query/sync.py +103 -0
  51. stint-0.1.0a0/stint/registry.py +81 -0
  52. stint-0.1.0a0/stint/schema/__init__.py +18 -0
  53. stint-0.1.0a0/stint/schema/_meta.py +49 -0
  54. stint-0.1.0a0/stint/schema/field_config.py +29 -0
  55. stint-0.1.0a0/stint/schema/issuetype.py +177 -0
  56. stint-0.1.0a0/stint/schema/project.py +91 -0
  57. stint-0.1.0a0/stint/schema/screen.py +53 -0
  58. stint-0.1.0a0/stint/state/__init__.py +6 -0
  59. stint-0.1.0a0/stint/state/file.py +187 -0
  60. stint-0.1.0a0/stint/state/lock.py +82 -0
  61. stint-0.1.0a0/stint/state/snapshot.py +160 -0
  62. stint-0.1.0a0/stint/validate.py +73 -0
  63. stint-0.1.0a0/stint.egg-info/PKG-INFO +268 -0
  64. stint-0.1.0a0/stint.egg-info/SOURCES.txt +87 -0
  65. stint-0.1.0a0/stint.egg-info/dependency_links.txt +1 -0
  66. stint-0.1.0a0/stint.egg-info/entry_points.txt +2 -0
  67. stint-0.1.0a0/stint.egg-info/requires.txt +9 -0
  68. stint-0.1.0a0/stint.egg-info/top_level.txt +1 -0
  69. stint-0.1.0a0/tests/test_m0_spike.py +427 -0
  70. stint-0.1.0a0/tests/test_m1_cli.py +179 -0
  71. stint-0.1.0a0/tests/test_m1_cloud.py +239 -0
  72. stint-0.1.0a0/tests/test_m1_dialect.py +544 -0
  73. stint-0.1.0a0/tests/test_m1_engine.py +78 -0
  74. stint-0.1.0a0/tests/test_m1_state.py +122 -0
  75. stint-0.1.0a0/tests/test_m2_downgrade.py +239 -0
  76. stint-0.1.0a0/tests/test_m2_migrations.py +276 -0
  77. stint-0.1.0a0/tests/test_m2_op_surface.py +770 -0
  78. stint-0.1.0a0/tests/test_m2_polish.py +528 -0
  79. stint-0.1.0a0/tests/test_m3_autogen.py +546 -0
  80. stint-0.1.0a0/tests/test_m3_autogenerate_cli.py +237 -0
  81. stint-0.1.0a0/tests/test_m3_multi_parent_graph.py +90 -0
  82. stint-0.1.0a0/tests/test_m3_revision_cli.py +150 -0
  83. stint-0.1.0a0/tests/test_m3_stamp.py +317 -0
  84. stint-0.1.0a0/tests/test_m4_polish.py +829 -0
  85. stint-0.1.0a0/tests/test_m5_query.py +425 -0
  86. stint-0.1.0a0/tests/test_m6_writes.py +457 -0
  87. stint-0.1.0a0/tests/test_m7_tmp.py +517 -0
  88. stint-0.1.0a0/tests/test_m8_sync_session.py +259 -0
  89. stint-0.1.0a0/tests/test_m8_validate_cli.py +157 -0
stint-0.1.0a0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Curtis Turner
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.
stint-0.1.0a0/PKG-INFO ADDED
@@ -0,0 +1,268 @@
1
+ Metadata-Version: 2.4
2
+ Name: stint
3
+ Version: 0.1.0a0
4
+ Summary: Declarative schema-as-code and ORM for work-management systems. Jira first, dialect protocol for the rest.
5
+ Author: Curtis Turner
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/curtis-turner/stint
8
+ Project-URL: Source, https://github.com/curtis-turner/stint
9
+ Project-URL: Issues, https://github.com/curtis-turner/stint/issues
10
+ Project-URL: Changelog, https://github.com/curtis-turner/stint/blob/main/CHANGELOG.md
11
+ Keywords: jira,orm,migrations,alembic,schema-as-code,atlassian
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: Database
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: pydantic>=2.0
27
+ Requires-Dist: pydantic-settings>=2.0
28
+ Requires-Dist: httpx>=0.25
29
+ Requires-Dist: pyyaml>=6.0
30
+ Requires-Dist: pydantic[email]
31
+ Requires-Dist: cyclopts>=3.0
32
+ Provides-Extra: jira
33
+ Requires-Dist: jira>=3.8; extra == "jira"
34
+ Dynamic: license-file
35
+
36
+ # stint
37
+
38
+ Declarative schema-as-code and ORM for work-management systems. Jira first.
39
+
40
+ stint is two tools in one, the same way SQLAlchemy + Alembic is.
41
+
42
+ 1. **Schema as code with versioned migrations.** Declare projects,
43
+ issuetypes, custom fields, screens, and schemes as Pydantic classes.
44
+ Autogenerate a migration from the diff between the declaration and the
45
+ live Jira instance. Commit the migration. Apply it across environments.
46
+ 2. **ORM.** Insert and query issues through the same model classes.
47
+ `session.add(Bug(...))`, `session.scalars(select(Bug).where(...))`.
48
+
49
+ Status: alpha (`0.1.0a0`). API may shift before `0.1.0` final.
50
+
51
+ **Primary target: Jira Cloud**, covering both company-managed and
52
+ team-managed projects. Live-instance validation against a real Cloud
53
+ tenant is the gating work for `0.1.0` final.
54
+
55
+ **Jira Data Center is out of scope for 0.1.** An audit against Atlassian's
56
+ official OpenAPI specs found that ~17 of the admin endpoints stint drives
57
+ exist only on Cloud; DC keeps those objects web-admin-only and never added
58
+ REST. Rather than ship a DC dialect that fails on half the op surface, 0.1
59
+ is Cloud-only. The dialect protocol stays the extension point, so a real DC
60
+ dialect (scoped to the endpoints DC actually exposes) can land in a later
61
+ release.
62
+
63
+ ## Install
64
+
65
+ ```bash
66
+ pip install stint
67
+ ```
68
+
69
+ Python 3.10 or newer. The only required runtime deps are `pydantic`,
70
+ `httpx`, and `pyyaml`.
71
+
72
+ ## Why
73
+
74
+ Jira admin lives in a web UI. That makes it hard to review changes, hard
75
+ to mirror across environments, and impossible to roll back cleanly.
76
+ stint treats Jira as a database where the schema happens to be
77
+ unusually heavy. The schema is Python you commit. Migrations are Python
78
+ you commit. Two environments get the same shape by running the same
79
+ migration chain.
80
+
81
+ The data plane is a separate concern. Once the schema is under
82
+ management, the same model classes are the ORM. `Bug.severity == "S1"`
83
+ compiles to `cf[10042] = "S1"` (or whatever the per-env id is, resolved
84
+ through the state file).
85
+
86
+ ## A small example
87
+
88
+ Define the schema as Pydantic classes:
89
+
90
+ ```python
91
+ # schemas/platform.py
92
+ from typing import Annotated, Literal
93
+
94
+ from stint import (
95
+ CustomField, IssueType, Project, Screen, ScreenScheme,
96
+ FieldConfiguration, SelectField, TextField,
97
+ )
98
+
99
+ severity_field = CustomField(
100
+ alias="bug_severity",
101
+ name="Severity",
102
+ type=SelectField,
103
+ options=["S1", "S2", "S3", "S4"],
104
+ )
105
+
106
+ bug_create = Screen(alias="bug_create", name="Bug Create",
107
+ fields=["Summary", "Reporter", severity_field])
108
+ bug_edit = Screen(alias="bug_edit", name="Bug Edit",
109
+ fields=["Summary", "Reporter", severity_field])
110
+ bug_view = Screen(alias="bug_view", name="Bug View",
111
+ fields=["Summary", "Reporter", severity_field])
112
+
113
+ bug_screens = ScreenScheme(
114
+ alias="bug_screens", name="Bug Screen Scheme",
115
+ create=bug_create, edit=bug_edit, view=bug_view,
116
+ )
117
+ bug_fields = FieldConfiguration(
118
+ alias="bug_fields", name="Bug Field Config", required=["Summary"],
119
+ )
120
+
121
+ class Bug(IssueType):
122
+ __alias__ = "bug"
123
+ __screen_scheme__ = bug_screens
124
+ __field_configuration__ = bug_fields
125
+
126
+ key: str | None = None
127
+ summary: str
128
+ reporter: str
129
+ severity: Annotated[Literal["S1", "S2", "S3", "S4"], severity_field]
130
+
131
+ class Platform(Project):
132
+ __key__ = "PLAT"
133
+ __lead__ = "cturner"
134
+ __style__ = "company-managed"
135
+ __issuetypes__ = [Bug]
136
+ ```
137
+
138
+ Validate it without any network call:
139
+
140
+ ```bash
141
+ stint validate --schema schemas/platform.py
142
+ ```
143
+
144
+ Generate a migration from the diff between your schema and the live env:
145
+
146
+ ```bash
147
+ export STINT_USER=you@example.com # Jira Cloud account email
148
+ export STINT_TOKEN=... # Jira Cloud API token
149
+ stint revision --autogenerate \
150
+ --schema schemas.platform \
151
+ --state state/dev.yaml \
152
+ --migrations-dir migrations/ \
153
+ --url jira_cloud+https://you.atlassian.net \
154
+ --auth api-token \
155
+ --env dev \
156
+ -m "add bug severity"
157
+ ```
158
+
159
+ Review the emitted Python file. It looks roughly like:
160
+
161
+ ```python
162
+ # migrations/2026_05_25_1430_add_bug_severity.py
163
+ from stint import op
164
+ from stint.fields import SelectField
165
+
166
+ revision = "abc123def456"
167
+ down_revision = None
168
+
169
+ async def upgrade():
170
+ await op.create_custom_field(
171
+ alias="bug_severity",
172
+ name="Severity",
173
+ type=SelectField,
174
+ options=["S1", "S2", "S3", "S4"],
175
+ )
176
+
177
+ async def downgrade():
178
+ op.unsupported("deleting bug_severity destroys severity data on existing issues")
179
+ ```
180
+
181
+ Apply it:
182
+
183
+ ```bash
184
+ stint upgrade --env dev
185
+ ```
186
+
187
+ Then query and write issues through the same classes:
188
+
189
+ ```python
190
+ from stint import Session, StateFile, APITokenAuth, create_engine, select
191
+ from schemas.platform import Bug
192
+
193
+ state = StateFile.load("state/dev.yaml")
194
+ engine = create_engine(
195
+ "jira_cloud+https://you.atlassian.net",
196
+ auth=APITokenAuth(email="you@example.com", token=token),
197
+ )
198
+
199
+ with Session(engine, state) as session:
200
+ # READ
201
+ s1s = session.scalars(select(Bug).where(Bug.c.severity == "S1"))
202
+
203
+ # WRITE
204
+ bug = Bug(summary="boom", reporter="alice", severity="S2")
205
+ session.add(bug)
206
+ session.commit()
207
+ print(bug.key) # populated from the response
208
+
209
+ # UPDATE
210
+ existing = session.get(Bug, "PLAT-1234")
211
+ existing.severity = "S1"
212
+ session.commit() # only the changed field is sent
213
+ ```
214
+
215
+ The async version is `AsyncSession(engine, state)` with the same surface
216
+ plus `await` on the I/O methods.
217
+
218
+ ## What ships in 0.1
219
+
220
+ - **Jira Cloud (only target in 0.1)**: company-managed and team-managed
221
+ projects. Live-tenant smoke is the gating work for `0.1.0` final.
222
+ Data Center is deferred to a later release (see the status note above).
223
+ - Schema plane: custom fields, screens, screen schemes, issue type
224
+ screen schemes, field configurations, field configuration schemes,
225
+ issue types, projects.
226
+ - Migrations: autogenerate, multi-head merge, upgrade, downgrade,
227
+ brownfield stamp, idempotent ops, advisory lock, `429`/`503` retry.
228
+ - Data plane: reads and writes, identity map, dirty tracking, ADF
229
+ wrapping for Cloud descriptions.
230
+ - Cloud team-managed projects: project create with style tracking, CMP-only
231
+ ops fail loud with a Jira UI deep link. Per-issuetype inline screen field
232
+ lists deferred until Atlassian exposes them through REST.
233
+
234
+ ## What does not ship in 0.1
235
+
236
+ - Workflows and workflow schemes.
237
+ - Permission and notification schemes.
238
+ - ADF parsing on reads (writes wrap plain text correctly).
239
+ - Backends other than Jira. The dialect protocol is the extension point.
240
+
241
+ ## CLI
242
+
243
+ ```
244
+ stint reflect Reflect a Jira instance into a snapshot and print it.
245
+ stint revision Create a migration (empty, --merge, or --autogenerate).
246
+ stint stamp Brownfield: match aliases against existing Jira, populate state.
247
+ stint upgrade Apply pending migrations to head (or --to <rev>).
248
+ stint downgrade Roll back to a specific revision.
249
+ stint current Show the env's current revision.
250
+ stint history List migrations in revision order.
251
+ stint validate Run schema-level checks on a Python schema module.
252
+ ```
253
+
254
+ ## Caveats
255
+
256
+ - **No transactions across or within migrations.** Jira admin REST does
257
+ not provide them. A failed migration leaves Jira in a partial state.
258
+ Re-running picks up where it left off because op functions check the
259
+ alias-to-id map first.
260
+ - **Partial commits in the data plane raise `PartialCommitError`.** Some
261
+ inserts can succeed and later ones fail; the exception carries the
262
+ per-instance breakdown so callers can decide what to do.
263
+ - **Drift is not auto-reverted.** UI edits land in the live instance and
264
+ stay there until you reflect, diff, and absorb them into the schema.
265
+
266
+ ## License
267
+
268
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,233 @@
1
+ # stint
2
+
3
+ Declarative schema-as-code and ORM for work-management systems. Jira first.
4
+
5
+ stint is two tools in one, the same way SQLAlchemy + Alembic is.
6
+
7
+ 1. **Schema as code with versioned migrations.** Declare projects,
8
+ issuetypes, custom fields, screens, and schemes as Pydantic classes.
9
+ Autogenerate a migration from the diff between the declaration and the
10
+ live Jira instance. Commit the migration. Apply it across environments.
11
+ 2. **ORM.** Insert and query issues through the same model classes.
12
+ `session.add(Bug(...))`, `session.scalars(select(Bug).where(...))`.
13
+
14
+ Status: alpha (`0.1.0a0`). API may shift before `0.1.0` final.
15
+
16
+ **Primary target: Jira Cloud**, covering both company-managed and
17
+ team-managed projects. Live-instance validation against a real Cloud
18
+ tenant is the gating work for `0.1.0` final.
19
+
20
+ **Jira Data Center is out of scope for 0.1.** An audit against Atlassian's
21
+ official OpenAPI specs found that ~17 of the admin endpoints stint drives
22
+ exist only on Cloud; DC keeps those objects web-admin-only and never added
23
+ REST. Rather than ship a DC dialect that fails on half the op surface, 0.1
24
+ is Cloud-only. The dialect protocol stays the extension point, so a real DC
25
+ dialect (scoped to the endpoints DC actually exposes) can land in a later
26
+ release.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install stint
32
+ ```
33
+
34
+ Python 3.10 or newer. The only required runtime deps are `pydantic`,
35
+ `httpx`, and `pyyaml`.
36
+
37
+ ## Why
38
+
39
+ Jira admin lives in a web UI. That makes it hard to review changes, hard
40
+ to mirror across environments, and impossible to roll back cleanly.
41
+ stint treats Jira as a database where the schema happens to be
42
+ unusually heavy. The schema is Python you commit. Migrations are Python
43
+ you commit. Two environments get the same shape by running the same
44
+ migration chain.
45
+
46
+ The data plane is a separate concern. Once the schema is under
47
+ management, the same model classes are the ORM. `Bug.severity == "S1"`
48
+ compiles to `cf[10042] = "S1"` (or whatever the per-env id is, resolved
49
+ through the state file).
50
+
51
+ ## A small example
52
+
53
+ Define the schema as Pydantic classes:
54
+
55
+ ```python
56
+ # schemas/platform.py
57
+ from typing import Annotated, Literal
58
+
59
+ from stint import (
60
+ CustomField, IssueType, Project, Screen, ScreenScheme,
61
+ FieldConfiguration, SelectField, TextField,
62
+ )
63
+
64
+ severity_field = CustomField(
65
+ alias="bug_severity",
66
+ name="Severity",
67
+ type=SelectField,
68
+ options=["S1", "S2", "S3", "S4"],
69
+ )
70
+
71
+ bug_create = Screen(alias="bug_create", name="Bug Create",
72
+ fields=["Summary", "Reporter", severity_field])
73
+ bug_edit = Screen(alias="bug_edit", name="Bug Edit",
74
+ fields=["Summary", "Reporter", severity_field])
75
+ bug_view = Screen(alias="bug_view", name="Bug View",
76
+ fields=["Summary", "Reporter", severity_field])
77
+
78
+ bug_screens = ScreenScheme(
79
+ alias="bug_screens", name="Bug Screen Scheme",
80
+ create=bug_create, edit=bug_edit, view=bug_view,
81
+ )
82
+ bug_fields = FieldConfiguration(
83
+ alias="bug_fields", name="Bug Field Config", required=["Summary"],
84
+ )
85
+
86
+ class Bug(IssueType):
87
+ __alias__ = "bug"
88
+ __screen_scheme__ = bug_screens
89
+ __field_configuration__ = bug_fields
90
+
91
+ key: str | None = None
92
+ summary: str
93
+ reporter: str
94
+ severity: Annotated[Literal["S1", "S2", "S3", "S4"], severity_field]
95
+
96
+ class Platform(Project):
97
+ __key__ = "PLAT"
98
+ __lead__ = "cturner"
99
+ __style__ = "company-managed"
100
+ __issuetypes__ = [Bug]
101
+ ```
102
+
103
+ Validate it without any network call:
104
+
105
+ ```bash
106
+ stint validate --schema schemas/platform.py
107
+ ```
108
+
109
+ Generate a migration from the diff between your schema and the live env:
110
+
111
+ ```bash
112
+ export STINT_USER=you@example.com # Jira Cloud account email
113
+ export STINT_TOKEN=... # Jira Cloud API token
114
+ stint revision --autogenerate \
115
+ --schema schemas.platform \
116
+ --state state/dev.yaml \
117
+ --migrations-dir migrations/ \
118
+ --url jira_cloud+https://you.atlassian.net \
119
+ --auth api-token \
120
+ --env dev \
121
+ -m "add bug severity"
122
+ ```
123
+
124
+ Review the emitted Python file. It looks roughly like:
125
+
126
+ ```python
127
+ # migrations/2026_05_25_1430_add_bug_severity.py
128
+ from stint import op
129
+ from stint.fields import SelectField
130
+
131
+ revision = "abc123def456"
132
+ down_revision = None
133
+
134
+ async def upgrade():
135
+ await op.create_custom_field(
136
+ alias="bug_severity",
137
+ name="Severity",
138
+ type=SelectField,
139
+ options=["S1", "S2", "S3", "S4"],
140
+ )
141
+
142
+ async def downgrade():
143
+ op.unsupported("deleting bug_severity destroys severity data on existing issues")
144
+ ```
145
+
146
+ Apply it:
147
+
148
+ ```bash
149
+ stint upgrade --env dev
150
+ ```
151
+
152
+ Then query and write issues through the same classes:
153
+
154
+ ```python
155
+ from stint import Session, StateFile, APITokenAuth, create_engine, select
156
+ from schemas.platform import Bug
157
+
158
+ state = StateFile.load("state/dev.yaml")
159
+ engine = create_engine(
160
+ "jira_cloud+https://you.atlassian.net",
161
+ auth=APITokenAuth(email="you@example.com", token=token),
162
+ )
163
+
164
+ with Session(engine, state) as session:
165
+ # READ
166
+ s1s = session.scalars(select(Bug).where(Bug.c.severity == "S1"))
167
+
168
+ # WRITE
169
+ bug = Bug(summary="boom", reporter="alice", severity="S2")
170
+ session.add(bug)
171
+ session.commit()
172
+ print(bug.key) # populated from the response
173
+
174
+ # UPDATE
175
+ existing = session.get(Bug, "PLAT-1234")
176
+ existing.severity = "S1"
177
+ session.commit() # only the changed field is sent
178
+ ```
179
+
180
+ The async version is `AsyncSession(engine, state)` with the same surface
181
+ plus `await` on the I/O methods.
182
+
183
+ ## What ships in 0.1
184
+
185
+ - **Jira Cloud (only target in 0.1)**: company-managed and team-managed
186
+ projects. Live-tenant smoke is the gating work for `0.1.0` final.
187
+ Data Center is deferred to a later release (see the status note above).
188
+ - Schema plane: custom fields, screens, screen schemes, issue type
189
+ screen schemes, field configurations, field configuration schemes,
190
+ issue types, projects.
191
+ - Migrations: autogenerate, multi-head merge, upgrade, downgrade,
192
+ brownfield stamp, idempotent ops, advisory lock, `429`/`503` retry.
193
+ - Data plane: reads and writes, identity map, dirty tracking, ADF
194
+ wrapping for Cloud descriptions.
195
+ - Cloud team-managed projects: project create with style tracking, CMP-only
196
+ ops fail loud with a Jira UI deep link. Per-issuetype inline screen field
197
+ lists deferred until Atlassian exposes them through REST.
198
+
199
+ ## What does not ship in 0.1
200
+
201
+ - Workflows and workflow schemes.
202
+ - Permission and notification schemes.
203
+ - ADF parsing on reads (writes wrap plain text correctly).
204
+ - Backends other than Jira. The dialect protocol is the extension point.
205
+
206
+ ## CLI
207
+
208
+ ```
209
+ stint reflect Reflect a Jira instance into a snapshot and print it.
210
+ stint revision Create a migration (empty, --merge, or --autogenerate).
211
+ stint stamp Brownfield: match aliases against existing Jira, populate state.
212
+ stint upgrade Apply pending migrations to head (or --to <rev>).
213
+ stint downgrade Roll back to a specific revision.
214
+ stint current Show the env's current revision.
215
+ stint history List migrations in revision order.
216
+ stint validate Run schema-level checks on a Python schema module.
217
+ ```
218
+
219
+ ## Caveats
220
+
221
+ - **No transactions across or within migrations.** Jira admin REST does
222
+ not provide them. A failed migration leaves Jira in a partial state.
223
+ Re-running picks up where it left off because op functions check the
224
+ alias-to-id map first.
225
+ - **Partial commits in the data plane raise `PartialCommitError`.** Some
226
+ inserts can succeed and later ones fail; the exception carries the
227
+ per-instance breakdown so callers can decide what to do.
228
+ - **Drift is not auto-reverted.** UI edits land in the live instance and
229
+ stay there until you reflect, diff, and absorb them into the schema.
230
+
231
+ ## License
232
+
233
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,81 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "stint"
7
+ version = "0.1.0a0"
8
+ description = "Declarative schema-as-code and ORM for work-management systems. Jira first, dialect protocol for the rest."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Curtis Turner" }]
13
+ keywords = [
14
+ "jira",
15
+ "orm",
16
+ "migrations",
17
+ "alembic",
18
+ "schema-as-code",
19
+ "atlassian",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 3 - Alpha",
23
+ "Intended Audience :: Developers",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Topic :: Software Development :: Libraries :: Python Modules",
31
+ "Topic :: Database",
32
+ "Typing :: Typed",
33
+ ]
34
+ dependencies = [
35
+ "pydantic>=2.0",
36
+ "pydantic-settings>=2.0",
37
+ "httpx>=0.25",
38
+ "pyyaml>=6.0",
39
+ "pydantic[email]",
40
+ "cyclopts>=3.0",
41
+ ]
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/curtis-turner/stint"
45
+ Source = "https://github.com/curtis-turner/stint"
46
+ Issues = "https://github.com/curtis-turner/stint/issues"
47
+ Changelog = "https://github.com/curtis-turner/stint/blob/main/CHANGELOG.md"
48
+
49
+ [project.optional-dependencies]
50
+ jira = ["jira>=3.8"]
51
+
52
+ [project.scripts]
53
+ stint = "stint.cli:main"
54
+
55
+ [tool.setuptools.packages.find]
56
+ include = ["stint*"]
57
+
58
+ [tool.setuptools.package-data]
59
+ stint = ["py.typed"]
60
+
61
+ [tool.ruff]
62
+ line-length = 120
63
+ target-version = "py310"
64
+
65
+ [tool.ruff.lint]
66
+ select = ["B", "E", "F", "I", "N", "W", "UP"]
67
+
68
+ [tool.pytest.ini_options]
69
+ testpaths = ["tests"]
70
+ asyncio_mode = "auto"
71
+
72
+ [dependency-groups]
73
+ dev = [
74
+ "pytest>=9.0.3",
75
+ "pytest-asyncio>=0.21",
76
+ "pytest-cov>=4.0",
77
+ "respx>=0.20",
78
+ "ruff>=0.15.14",
79
+ "mypy>=1.0",
80
+ "types-PyYAML>=6.0",
81
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+