flowgrate 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.
@@ -0,0 +1,33 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ name: Test (Python ${{ matrix.python-version }})
12
+ runs-on: ubuntu-latest
13
+
14
+ strategy:
15
+ matrix:
16
+ python-version: ["3.10", "3.11", "3.12"]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Set up Python ${{ matrix.python-version }}
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+
26
+ - name: Install dependencies
27
+ run: |
28
+ python -m pip install --upgrade pip
29
+ pip install pytest
30
+ pip install -e .
31
+
32
+ - name: Run tests
33
+ run: pytest tests/ -v
@@ -0,0 +1,41 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ name: Build and publish
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Install build tools
22
+ run: pip install hatchling build
23
+
24
+ - name: Set version from tag
25
+ run: |
26
+ VERSION=${GITHUB_REF_NAME#v}
27
+ sed -i "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
28
+
29
+ - name: Run tests
30
+ run: |
31
+ pip install pytest
32
+ pip install -e .
33
+ pytest tests/ -v
34
+
35
+ - name: Build package
36
+ run: python -m build
37
+
38
+ - name: Publish to PyPI
39
+ uses: pypa/gh-action-pypi-publish@release/v1
40
+ with:
41
+ password: ${{ secrets.PYPI_API_KEY }}
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ venv/
9
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 flowgrate
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.
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: flowgrate
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Flowgrate — Laravel-style database migrations with a fluent API
5
+ Project-URL: Homepage, https://github.com/flowgrate/python
6
+ Project-URL: Repository, https://github.com/flowgrate/python
7
+ Project-URL: Issues, https://github.com/flowgrate/python/issues
8
+ Author-email: flowgrate <hi@flowgrate.dev>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: database,fluent,migrations,postgresql,schema
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: Topic :: Database
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+
24
+ # flowgrate/python
25
+
26
+ Python SDK for [Flowgrate](https://github.com/flowgrate/core) — Laravel-style database migrations with a fluent API.
27
+
28
+ ## How it works
29
+
30
+ Define migrations in Python using the fluent Blueprint API. The SDK serializes them to JSON and pipes to the Flowgrate CLI, which compiles and executes the SQL.
31
+
32
+ ## Requirements
33
+
34
+ - Python 3.10+
35
+ - [Flowgrate CLI](https://github.com/flowgrate/core/releases) — download the binary for your platform and put it on your `PATH`
36
+
37
+ ```bash
38
+ # Linux (amd64)
39
+ curl -L https://github.com/flowgrate/core/releases/latest/download/flowgrate-linux-amd64 -o flowgrate
40
+ chmod +x flowgrate
41
+ sudo mv flowgrate /usr/local/bin/
42
+
43
+ # macOS (Apple Silicon)
44
+ curl -L https://github.com/flowgrate/core/releases/latest/download/flowgrate-darwin-arm64 -o flowgrate
45
+ chmod +x flowgrate
46
+ sudo mv flowgrate /usr/local/bin/
47
+
48
+ # macOS (Intel)
49
+ curl -L https://github.com/flowgrate/core/releases/latest/download/flowgrate-darwin-amd64 -o flowgrate
50
+ chmod +x flowgrate
51
+ sudo mv flowgrate /usr/local/bin/
52
+
53
+ # Or build from source
54
+ go install github.com/flowgrate/core@latest
55
+ ```
56
+
57
+ ## Setup
58
+
59
+ **1. Install the SDK:**
60
+
61
+ ```bash
62
+ pip install flowgrate
63
+ ```
64
+
65
+ **2. Create `Program.py` (entry point for the CLI to invoke):**
66
+
67
+ ```python
68
+ from flowgrate import FlowgrateRunner
69
+ import os
70
+
71
+ FlowgrateRunner.run(os.path.dirname(__file__))
72
+ ```
73
+
74
+ **3. Create `flowgrate.yml` next to your migrations:**
75
+
76
+ Generate it with the CLI (recommended):
77
+
78
+ ```bash
79
+ flowgrate init --db=postgres://user:pass@localhost/mydb
80
+ ```
81
+
82
+ Or create manually:
83
+
84
+ ```yaml
85
+ database:
86
+ url: postgres://user:pass@localhost/mydb
87
+
88
+ migrations:
89
+ project: ./migrations
90
+ sdk: python
91
+ run: python Program.py
92
+ ```
93
+
94
+ **4. Generate and run migrations:**
95
+
96
+ ```bash
97
+ flowgrate make CreateUsersTable
98
+ flowgrate up
99
+ ```
100
+
101
+ ## Migration anatomy
102
+
103
+ ```python
104
+ from flowgrate import Migration, Schema
105
+
106
+
107
+ class CreateUsersTable(Migration):
108
+ def up(self):
109
+ with Schema.create("users") as t:
110
+ t.id()
111
+ t.string("name")
112
+ t.string("email", 100)
113
+ t.timestamps()
114
+
115
+ def down(self):
116
+ Schema.drop_if_exists("users")
117
+ ```
118
+
119
+ Migration files must follow the naming convention: `YYYYMMDD_HHMMSS_MigrationName.py`
120
+
121
+ ## Blueprint API reference
122
+
123
+ ### Create / drop table
124
+
125
+ ```python
126
+ Schema.create("users") # CREATE TABLE
127
+ Schema.table("users") # ALTER TABLE
128
+ Schema.drop("users") # DROP TABLE
129
+ Schema.drop_if_exists("users") # DROP TABLE IF EXISTS
130
+ ```
131
+
132
+ ### Column types
133
+
134
+ ```python
135
+ t.id() # BIGSERIAL PRIMARY KEY
136
+ t.small_integer("level") # SMALLINT
137
+ t.integer("views") # INTEGER
138
+ t.big_integer("score") # BIGINT
139
+ t.decimal("price", 10, 2) # NUMERIC(10, 2)
140
+ t.float("rating") # REAL
141
+ t.double("latitude") # DOUBLE PRECISION
142
+ t.boolean("active") # BOOLEAN
143
+ t.string("name") # VARCHAR(255)
144
+ t.string("code", 10) # VARCHAR(10)
145
+ t.text("bio") # TEXT
146
+ t.uuid("public_id") # UUID
147
+ t.json("settings") # JSON
148
+ t.jsonb("metadata") # JSONB
149
+ t.binary("avatar") # BYTEA
150
+ t.date("birthday") # DATE
151
+ t.time("opens_at") # TIME
152
+ t.timestamp("verified_at") # TIMESTAMP
153
+ ```
154
+
155
+ ### Column modifiers (chainable)
156
+
157
+ ```python
158
+ .nullable() # NULL
159
+ .default(value) # DEFAULT value
160
+ .default_expression("NOW()") # DEFAULT NOW() — raw SQL
161
+ .generated_uuid() # DEFAULT gen_random_uuid()
162
+ .comment("description")
163
+ .unique() # single-column unique index
164
+ ```
165
+
166
+ ### Helpers
167
+
168
+ ```python
169
+ t.timestamps() # created_at + updated_at TIMESTAMP DEFAULT NOW()
170
+ t.soft_deletes() # deleted_at TIMESTAMP NULL
171
+ t.remember_token() # remember_token VARCHAR(100) NULL
172
+ t.polymorphic("commentable") # commentable_id BIGINT + commentable_type VARCHAR(255) + index
173
+ t.nullable_polymorphic("taggable")
174
+ ```
175
+
176
+ ### Foreign keys
177
+
178
+ ```python
179
+ t.foreign_id("role_id") \
180
+ .constrained("roles") \
181
+ .on_delete("cascade") \
182
+ .on_update("cascade")
183
+ ```
184
+
185
+ ### Indexes
186
+
187
+ ```python
188
+ t.unique("email", "tenant_id", name="uq_users_email_tenant")
189
+ t.index("created_at")
190
+ t.index("email", "name", name="idx_users_search")
191
+ ```
192
+
193
+ ### ALTER TABLE
194
+
195
+ ```python
196
+ with Schema.table("users") as t:
197
+ t.add_column("phone").string(20).nullable()
198
+ t.change_column("name").string(500)
199
+ t.drop_column("avatar")
200
+ ```
201
+
202
+ ## Running in Docker
203
+
204
+ ```bash
205
+ docker compose exec sdk python Program.py | flowgrate up
206
+ ```
@@ -0,0 +1,183 @@
1
+ # flowgrate/python
2
+
3
+ Python SDK for [Flowgrate](https://github.com/flowgrate/core) — Laravel-style database migrations with a fluent API.
4
+
5
+ ## How it works
6
+
7
+ Define migrations in Python using the fluent Blueprint API. The SDK serializes them to JSON and pipes to the Flowgrate CLI, which compiles and executes the SQL.
8
+
9
+ ## Requirements
10
+
11
+ - Python 3.10+
12
+ - [Flowgrate CLI](https://github.com/flowgrate/core/releases) — download the binary for your platform and put it on your `PATH`
13
+
14
+ ```bash
15
+ # Linux (amd64)
16
+ curl -L https://github.com/flowgrate/core/releases/latest/download/flowgrate-linux-amd64 -o flowgrate
17
+ chmod +x flowgrate
18
+ sudo mv flowgrate /usr/local/bin/
19
+
20
+ # macOS (Apple Silicon)
21
+ curl -L https://github.com/flowgrate/core/releases/latest/download/flowgrate-darwin-arm64 -o flowgrate
22
+ chmod +x flowgrate
23
+ sudo mv flowgrate /usr/local/bin/
24
+
25
+ # macOS (Intel)
26
+ curl -L https://github.com/flowgrate/core/releases/latest/download/flowgrate-darwin-amd64 -o flowgrate
27
+ chmod +x flowgrate
28
+ sudo mv flowgrate /usr/local/bin/
29
+
30
+ # Or build from source
31
+ go install github.com/flowgrate/core@latest
32
+ ```
33
+
34
+ ## Setup
35
+
36
+ **1. Install the SDK:**
37
+
38
+ ```bash
39
+ pip install flowgrate
40
+ ```
41
+
42
+ **2. Create `Program.py` (entry point for the CLI to invoke):**
43
+
44
+ ```python
45
+ from flowgrate import FlowgrateRunner
46
+ import os
47
+
48
+ FlowgrateRunner.run(os.path.dirname(__file__))
49
+ ```
50
+
51
+ **3. Create `flowgrate.yml` next to your migrations:**
52
+
53
+ Generate it with the CLI (recommended):
54
+
55
+ ```bash
56
+ flowgrate init --db=postgres://user:pass@localhost/mydb
57
+ ```
58
+
59
+ Or create manually:
60
+
61
+ ```yaml
62
+ database:
63
+ url: postgres://user:pass@localhost/mydb
64
+
65
+ migrations:
66
+ project: ./migrations
67
+ sdk: python
68
+ run: python Program.py
69
+ ```
70
+
71
+ **4. Generate and run migrations:**
72
+
73
+ ```bash
74
+ flowgrate make CreateUsersTable
75
+ flowgrate up
76
+ ```
77
+
78
+ ## Migration anatomy
79
+
80
+ ```python
81
+ from flowgrate import Migration, Schema
82
+
83
+
84
+ class CreateUsersTable(Migration):
85
+ def up(self):
86
+ with Schema.create("users") as t:
87
+ t.id()
88
+ t.string("name")
89
+ t.string("email", 100)
90
+ t.timestamps()
91
+
92
+ def down(self):
93
+ Schema.drop_if_exists("users")
94
+ ```
95
+
96
+ Migration files must follow the naming convention: `YYYYMMDD_HHMMSS_MigrationName.py`
97
+
98
+ ## Blueprint API reference
99
+
100
+ ### Create / drop table
101
+
102
+ ```python
103
+ Schema.create("users") # CREATE TABLE
104
+ Schema.table("users") # ALTER TABLE
105
+ Schema.drop("users") # DROP TABLE
106
+ Schema.drop_if_exists("users") # DROP TABLE IF EXISTS
107
+ ```
108
+
109
+ ### Column types
110
+
111
+ ```python
112
+ t.id() # BIGSERIAL PRIMARY KEY
113
+ t.small_integer("level") # SMALLINT
114
+ t.integer("views") # INTEGER
115
+ t.big_integer("score") # BIGINT
116
+ t.decimal("price", 10, 2) # NUMERIC(10, 2)
117
+ t.float("rating") # REAL
118
+ t.double("latitude") # DOUBLE PRECISION
119
+ t.boolean("active") # BOOLEAN
120
+ t.string("name") # VARCHAR(255)
121
+ t.string("code", 10) # VARCHAR(10)
122
+ t.text("bio") # TEXT
123
+ t.uuid("public_id") # UUID
124
+ t.json("settings") # JSON
125
+ t.jsonb("metadata") # JSONB
126
+ t.binary("avatar") # BYTEA
127
+ t.date("birthday") # DATE
128
+ t.time("opens_at") # TIME
129
+ t.timestamp("verified_at") # TIMESTAMP
130
+ ```
131
+
132
+ ### Column modifiers (chainable)
133
+
134
+ ```python
135
+ .nullable() # NULL
136
+ .default(value) # DEFAULT value
137
+ .default_expression("NOW()") # DEFAULT NOW() — raw SQL
138
+ .generated_uuid() # DEFAULT gen_random_uuid()
139
+ .comment("description")
140
+ .unique() # single-column unique index
141
+ ```
142
+
143
+ ### Helpers
144
+
145
+ ```python
146
+ t.timestamps() # created_at + updated_at TIMESTAMP DEFAULT NOW()
147
+ t.soft_deletes() # deleted_at TIMESTAMP NULL
148
+ t.remember_token() # remember_token VARCHAR(100) NULL
149
+ t.polymorphic("commentable") # commentable_id BIGINT + commentable_type VARCHAR(255) + index
150
+ t.nullable_polymorphic("taggable")
151
+ ```
152
+
153
+ ### Foreign keys
154
+
155
+ ```python
156
+ t.foreign_id("role_id") \
157
+ .constrained("roles") \
158
+ .on_delete("cascade") \
159
+ .on_update("cascade")
160
+ ```
161
+
162
+ ### Indexes
163
+
164
+ ```python
165
+ t.unique("email", "tenant_id", name="uq_users_email_tenant")
166
+ t.index("created_at")
167
+ t.index("email", "name", name="idx_users_search")
168
+ ```
169
+
170
+ ### ALTER TABLE
171
+
172
+ ```python
173
+ with Schema.table("users") as t:
174
+ t.add_column("phone").string(20).nullable()
175
+ t.change_column("name").string(500)
176
+ t.drop_column("avatar")
177
+ ```
178
+
179
+ ## Running in Docker
180
+
181
+ ```bash
182
+ docker compose exec sdk python Program.py | flowgrate up
183
+ ```
@@ -0,0 +1,5 @@
1
+ from .migration import Migration
2
+ from .runner import FlowgrateRunner
3
+ from .schema import Schema, Blueprint, ColumnBuilder, ColumnType
4
+
5
+ __all__ = ["Migration", "FlowgrateRunner", "Schema", "Blueprint", "ColumnBuilder", "ColumnType"]
@@ -0,0 +1,9 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class Migration(ABC):
5
+ @abstractmethod
6
+ def up(self) -> None: ...
7
+
8
+ @abstractmethod
9
+ def down(self) -> None: ...
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+ import importlib.util
3
+ import inspect
4
+ import json
5
+ import os
6
+ import re
7
+ import sys
8
+
9
+ from .migration import Migration
10
+ from .schema.schema import Schema, SchemaOperation
11
+ from .schema.types import ColumnDefinition, ColumnAction, IndexDefinition, ForeignKeyDefinition
12
+
13
+
14
+ _MIGRATION_PATTERN = re.compile(r"^\d{8}_\d{6}_\w+\.py$")
15
+
16
+
17
+ def main() -> None:
18
+ """Entry point: run migrations from the current directory."""
19
+ directory = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.abspath(sys.argv[0]))
20
+ FlowgrateRunner.run(directory)
21
+
22
+
23
+ class FlowgrateRunner:
24
+ @classmethod
25
+ def run(cls, directory: str) -> None:
26
+ """Discover all migrations in directory, serialize to JSON and write to stdout."""
27
+ migrations = cls._discover(directory)
28
+ for name, migration_cls in migrations:
29
+ manifest = cls._build_manifest(name, migration_cls())
30
+ print(json.dumps(manifest, ensure_ascii=False))
31
+
32
+ @classmethod
33
+ def _discover(cls, directory: str) -> list[tuple[str, type[Migration]]]:
34
+ directory = os.path.abspath(directory)
35
+ files = sorted(
36
+ f for f in os.listdir(directory)
37
+ if _MIGRATION_PATTERN.match(f)
38
+ )
39
+ result = []
40
+ for filename in files:
41
+ path = os.path.join(directory, filename)
42
+ migration_cls = cls._load(path)
43
+ if migration_cls:
44
+ name = os.path.splitext(filename)[0]
45
+ result.append((name, migration_cls))
46
+ return result
47
+
48
+ @classmethod
49
+ def _load(cls, path: str) -> type[Migration] | None:
50
+ spec = importlib.util.spec_from_file_location("_migration", path)
51
+ if spec is None or spec.loader is None:
52
+ return None
53
+ module = importlib.util.module_from_spec(spec)
54
+ spec.loader.exec_module(module) # type: ignore[union-attr]
55
+
56
+ for _, obj in inspect.getmembers(module, inspect.isclass):
57
+ if issubclass(obj, Migration) and obj is not Migration:
58
+ return obj
59
+ return None
60
+
61
+ @classmethod
62
+ def _build_manifest(cls, name: str, migration: Migration) -> dict:
63
+ Schema.reset()
64
+ migration.up()
65
+ up = [cls._serialize_op(op) for op in Schema._operations]
66
+
67
+ Schema.reset()
68
+ migration.down()
69
+ down = [cls._serialize_op(op) for op in Schema._operations]
70
+
71
+ Schema.reset()
72
+ return {"migration": name, "up": up, "down": down}
73
+
74
+ @classmethod
75
+ def _serialize_op(cls, op: SchemaOperation) -> dict:
76
+ result: dict = {"action": op.action, "table": op.table}
77
+
78
+ if op.if_exists:
79
+ result["if_exists"] = True
80
+
81
+ if op.blueprint:
82
+ bp = op.blueprint
83
+ is_alter = op.action == "alter_table"
84
+ if bp.columns:
85
+ result["columns"] = [cls._serialize_col(c, is_alter) for c in bp.columns]
86
+ if bp.indexes:
87
+ result["indexes"] = [cls._serialize_idx(i) for i in bp.indexes]
88
+ if bp.foreign_keys:
89
+ result["foreign_keys"] = [cls._serialize_fk(f) for f in bp.foreign_keys]
90
+
91
+ return result
92
+
93
+ @classmethod
94
+ def _serialize_col(cls, col: ColumnDefinition, in_alter: bool = False) -> dict:
95
+ d: dict = {"name": col.name, "type": col.type.value}
96
+
97
+ if in_alter or col.action != ColumnAction.ADD:
98
+ d["column_action"] = col.action.value
99
+ if col.length is not None:
100
+ d["length"] = col.length
101
+ if col.precision is not None:
102
+ d["precision"] = col.precision
103
+ if col.scale is not None:
104
+ d["scale"] = col.scale
105
+ if col.nullable:
106
+ d["nullable"] = True
107
+ if col.primary:
108
+ d["primary"] = True
109
+ if col.auto_increment:
110
+ d["auto_increment"] = True
111
+ if col.has_default:
112
+ if col.is_default_expression:
113
+ d["default_expression"] = col.default_value
114
+ else:
115
+ d["default"] = col.default_value
116
+ if col.comment:
117
+ d["comment"] = col.comment
118
+
119
+ return d
120
+
121
+ @classmethod
122
+ def _serialize_idx(cls, idx: IndexDefinition) -> dict:
123
+ d: dict = {"columns": idx.columns}
124
+ if idx.unique:
125
+ d["unique"] = True
126
+ if idx.name:
127
+ d["name"] = idx.name
128
+ return d
129
+
130
+ @classmethod
131
+ def _serialize_fk(cls, fk: ForeignKeyDefinition) -> dict:
132
+ d: dict = {
133
+ "column": fk.column,
134
+ "references_table": fk.references_table,
135
+ "references_column": fk.references_column,
136
+ }
137
+ if fk.on_update:
138
+ d["on_update"] = fk.on_update
139
+ if fk.on_delete:
140
+ d["on_delete"] = fk.on_delete
141
+ return d
@@ -0,0 +1,11 @@
1
+ from .types import ColumnType, ColumnAction, ColumnDefinition, IndexDefinition, ForeignKeyDefinition
2
+ from .blueprint import Blueprint
3
+ from .column_builder import ColumnBuilder
4
+ from .schema import Schema, SchemaOperation
5
+
6
+ __all__ = [
7
+ "ColumnType", "ColumnAction", "ColumnDefinition",
8
+ "IndexDefinition", "ForeignKeyDefinition",
9
+ "Blueprint", "ColumnBuilder",
10
+ "Schema", "SchemaOperation",
11
+ ]