jetbase 0.7.0__py3-none-any.whl → 0.12.1__py3-none-any.whl
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.
- jetbase/cli/main.py +146 -4
- jetbase/commands/current.py +20 -0
- jetbase/commands/fix_checksums.py +172 -0
- jetbase/commands/fix_files.py +133 -0
- jetbase/commands/history.py +53 -0
- jetbase/commands/init.py +29 -0
- jetbase/commands/lock_status.py +25 -0
- jetbase/commands/new.py +65 -0
- jetbase/commands/rollback.py +172 -0
- jetbase/commands/status.py +212 -0
- jetbase/commands/unlock.py +29 -0
- jetbase/commands/upgrade.py +248 -0
- jetbase/commands/validators.py +37 -0
- jetbase/config.py +304 -25
- jetbase/constants.py +10 -2
- jetbase/database/connection.py +40 -0
- jetbase/database/queries/base.py +353 -0
- jetbase/database/queries/default_queries.py +215 -0
- jetbase/database/queries/postgres.py +14 -0
- jetbase/database/queries/query_loader.py +87 -0
- jetbase/database/queries/sqlite.py +197 -0
- jetbase/engine/checksum.py +25 -0
- jetbase/engine/dry_run.py +105 -0
- jetbase/engine/file_parser.py +324 -0
- jetbase/engine/formatters.py +61 -0
- jetbase/engine/lock.py +65 -0
- jetbase/engine/repeatable.py +125 -0
- jetbase/engine/validation.py +238 -0
- jetbase/engine/version.py +144 -0
- jetbase/enums.py +37 -1
- jetbase/exceptions.py +87 -0
- jetbase/models.py +45 -0
- jetbase/repositories/lock_repo.py +129 -0
- jetbase/repositories/migrations_repo.py +451 -0
- jetbase-0.12.1.dist-info/METADATA +135 -0
- jetbase-0.12.1.dist-info/RECORD +39 -0
- {jetbase-0.7.0.dist-info → jetbase-0.12.1.dist-info}/WHEEL +1 -1
- jetbase/core/dry_run.py +0 -38
- jetbase/core/file_parser.py +0 -199
- jetbase/core/initialize.py +0 -33
- jetbase/core/repository.py +0 -169
- jetbase/core/rollback.py +0 -67
- jetbase/core/upgrade.py +0 -75
- jetbase/core/version.py +0 -163
- jetbase/queries.py +0 -72
- jetbase-0.7.0.dist-info/METADATA +0 -12
- jetbase-0.7.0.dist-info/RECORD +0 -17
- {jetbase-0.7.0.dist-info → jetbase-0.12.1.dist-info}/entry_points.txt +0 -0
- {jetbase-0.7.0.dist-info → jetbase-0.12.1.dist-info}/licenses/LICENSE +0 -0
jetbase/config.py
CHANGED
|
@@ -1,25 +1,199 @@
|
|
|
1
1
|
import importlib.machinery
|
|
2
2
|
import importlib.util
|
|
3
3
|
import os
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
6
|
from types import ModuleType
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import tomli
|
|
10
|
+
|
|
11
|
+
from jetbase.constants import ENV_FILE
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class JetbaseConfig:
|
|
16
|
+
"""
|
|
17
|
+
Configuration settings for Jetbase migrations.
|
|
18
|
+
|
|
19
|
+
This dataclass holds all configuration values loaded from env.py,
|
|
20
|
+
environment variables, or TOML configuration files.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
sqlalchemy_url (str): The SQLAlchemy database connection URL.
|
|
24
|
+
postgres_schema (str | None): Optional PostgreSQL schema to use.
|
|
25
|
+
Defaults to None.
|
|
26
|
+
skip_checksum_validation (bool): If True, skips validation that
|
|
27
|
+
migration files haven't been modified. Defaults to False.
|
|
28
|
+
skip_file_validation (bool): If True, skips validation that all
|
|
29
|
+
migration files exist. Defaults to False.
|
|
30
|
+
skip_validation (bool): If True, skips all validations.
|
|
31
|
+
Defaults to False.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
TypeError: If any boolean field receives a non-boolean value.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
sqlalchemy_url: str
|
|
38
|
+
postgres_schema: str | None = None
|
|
39
|
+
skip_checksum_validation: bool = False
|
|
40
|
+
skip_file_validation: bool = False
|
|
41
|
+
skip_validation: bool = False
|
|
42
|
+
|
|
43
|
+
def __post_init__(self):
|
|
44
|
+
# Validate skip_checksum_validation
|
|
45
|
+
if not isinstance(self.skip_checksum_validation, bool):
|
|
46
|
+
raise TypeError(
|
|
47
|
+
f"skip_checksum_validation must be bool, got {type(self.skip_checksum_validation).__name__}. "
|
|
48
|
+
f"Value: {self.skip_checksum_validation!r}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Validate skip_file_validation
|
|
52
|
+
if not isinstance(self.skip_file_validation, bool):
|
|
53
|
+
raise TypeError(
|
|
54
|
+
f"skip_file_validation must be bool, got {type(self.skip_file_validation).__name__}. "
|
|
55
|
+
f"Value: {self.skip_file_validation!r}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Validate skip_validation
|
|
59
|
+
if not isinstance(self.skip_validation, bool):
|
|
60
|
+
raise TypeError(
|
|
61
|
+
f"skip_validation must be bool, got {type(self.skip_validation).__name__}. "
|
|
62
|
+
f"Value: {self.skip_validation!r}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
ALL_KEYS: list[str] = [
|
|
67
|
+
field.name for field in JetbaseConfig.__dataclass_fields__.values()
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
DEFAULT_VALUES: dict[str, Any] = {
|
|
72
|
+
"skip_checksum_validation": False,
|
|
73
|
+
"skip_file_validation": False,
|
|
74
|
+
"skip_validation": False,
|
|
75
|
+
"postgres_schema": None,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
REQUIRED_KEYS: set[str] = {
|
|
79
|
+
"sqlalchemy_url",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_config(
|
|
84
|
+
keys: list[str] = ALL_KEYS,
|
|
85
|
+
defaults: dict[str, Any] | None = DEFAULT_VALUES,
|
|
86
|
+
required: set[str] | None = None,
|
|
87
|
+
) -> JetbaseConfig:
|
|
88
|
+
"""
|
|
89
|
+
Load configuration from env.py, environment variables, or TOML files.
|
|
90
|
+
|
|
91
|
+
Searches for configuration values in the following priority order:
|
|
92
|
+
1. env.py file in the current directory
|
|
93
|
+
2. Environment variables (JETBASE_{KEY_IN_UPPERCASE})
|
|
94
|
+
3. jetbase.toml file
|
|
95
|
+
4. pyproject.toml [tool.jetbase] section
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
keys (list[str]): List of configuration keys to retrieve.
|
|
99
|
+
Defaults to ALL_KEYS.
|
|
100
|
+
defaults (dict[str, Any] | None): Dictionary of default values
|
|
101
|
+
for optional configuration keys. Defaults to DEFAULT_VALUES.
|
|
102
|
+
required (set[str] | None): Set of keys that must be found.
|
|
103
|
+
Defaults to None.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
JetbaseConfig: Configuration dataclass with all requested values.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
ValueError: If a required key is not found in any configuration source.
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
>>> config = get_config(required={"sqlalchemy_url"})
|
|
113
|
+
>>> config.sqlalchemy_url
|
|
114
|
+
'postgresql://localhost/mydb'
|
|
115
|
+
"""
|
|
116
|
+
defaults = defaults or {}
|
|
117
|
+
required = required or set()
|
|
118
|
+
result: dict[str, Any] = {}
|
|
119
|
+
|
|
120
|
+
for key in keys:
|
|
121
|
+
value = _get_config_value(key)
|
|
122
|
+
|
|
123
|
+
if value is not None:
|
|
124
|
+
result[key] = value
|
|
125
|
+
elif key in defaults:
|
|
126
|
+
result[key] = defaults[key]
|
|
127
|
+
elif key in required:
|
|
128
|
+
raise ValueError(_get_config_help_message(key))
|
|
129
|
+
else:
|
|
130
|
+
result[key] = None
|
|
131
|
+
|
|
132
|
+
config = JetbaseConfig(**result)
|
|
133
|
+
return config
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _get_config_value(key: str) -> Any | None:
|
|
137
|
+
"""
|
|
138
|
+
Get a configuration value from all sources in priority order.
|
|
139
|
+
|
|
140
|
+
Checks env.py, environment variables, jetbase.toml, and pyproject.toml
|
|
141
|
+
in that order, returning the first value found.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
key (str): The configuration key to retrieve.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Any | None: The configuration value from the first available source,
|
|
148
|
+
or None if not found in any source.
|
|
149
|
+
"""
|
|
150
|
+
# Try env.py
|
|
151
|
+
value = _get_config_from_env_py(key)
|
|
152
|
+
if value is not None:
|
|
153
|
+
return value
|
|
154
|
+
|
|
155
|
+
# Try environment variable
|
|
156
|
+
value = _get_config_from_env_var(key)
|
|
157
|
+
if value is not None:
|
|
158
|
+
return value
|
|
159
|
+
|
|
160
|
+
# Try jetbase.toml
|
|
161
|
+
value = _get_config_from_jetbase_toml(key)
|
|
162
|
+
if value is not None:
|
|
163
|
+
return value
|
|
164
|
+
|
|
165
|
+
# Try pyproject.toml
|
|
166
|
+
pyproject_dir = _find_pyproject_toml()
|
|
167
|
+
if pyproject_dir:
|
|
168
|
+
value = _get_config_from_pyproject_toml(
|
|
169
|
+
key=key, filepath=pyproject_dir / "pyproject.toml"
|
|
170
|
+
)
|
|
171
|
+
if value is not None:
|
|
172
|
+
return value
|
|
173
|
+
|
|
174
|
+
return None
|
|
10
175
|
|
|
11
176
|
|
|
12
|
-
def
|
|
177
|
+
def _get_config_from_env_py(key: str, filepath: str = ENV_FILE) -> Any | None:
|
|
13
178
|
"""
|
|
14
|
-
Load a
|
|
179
|
+
Load a configuration value from the env.py file.
|
|
180
|
+
|
|
181
|
+
Dynamically imports the env.py file and retrieves the specified attribute.
|
|
15
182
|
|
|
16
183
|
Args:
|
|
17
|
-
|
|
184
|
+
key (str): The configuration key to retrieve.
|
|
185
|
+
filepath (str): Path to the env.py file relative to current directory.
|
|
186
|
+
Defaults to ENV_FILE.
|
|
18
187
|
|
|
19
188
|
Returns:
|
|
20
|
-
|
|
189
|
+
Any | None: The configuration value if the attribute exists,
|
|
190
|
+
otherwise None.
|
|
21
191
|
"""
|
|
22
|
-
config_path: str = os.path.join(os.getcwd(),
|
|
192
|
+
config_path: str = os.path.join(os.getcwd(), filepath)
|
|
193
|
+
|
|
194
|
+
if not os.path.exists(config_path):
|
|
195
|
+
return None
|
|
196
|
+
|
|
23
197
|
spec: importlib.machinery.ModuleSpec | None = (
|
|
24
198
|
importlib.util.spec_from_file_location("config", config_path)
|
|
25
199
|
)
|
|
@@ -30,31 +204,136 @@ def get_sqlalchemy_url(filename: str = CONFIG_FILE) -> str:
|
|
|
30
204
|
config: ModuleType = importlib.util.module_from_spec(spec)
|
|
31
205
|
spec.loader.exec_module(module=config)
|
|
32
206
|
|
|
33
|
-
|
|
207
|
+
config_value: Any | None = getattr(config, key, None)
|
|
34
208
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
209
|
+
return config_value
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _get_config_from_jetbase_toml(
|
|
213
|
+
key: str, filepath: str = "jetbase.toml"
|
|
214
|
+
) -> Any | None:
|
|
215
|
+
"""
|
|
216
|
+
Load a configuration value from the jetbase.toml file.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
key (str): The configuration key to retrieve.
|
|
220
|
+
filepath (str): Path to the jetbase.toml file. Defaults to "jetbase.toml".
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Any | None: The configuration value if found, otherwise None.
|
|
224
|
+
"""
|
|
225
|
+
if not os.path.exists(filepath):
|
|
226
|
+
return None
|
|
39
227
|
|
|
40
|
-
|
|
228
|
+
with open(filepath, "rb") as f:
|
|
229
|
+
jetbase_data = tomli.load(f)
|
|
41
230
|
|
|
42
|
-
|
|
231
|
+
config_value: Any = jetbase_data.get(key, None)
|
|
43
232
|
|
|
233
|
+
return config_value
|
|
44
234
|
|
|
45
|
-
|
|
235
|
+
|
|
236
|
+
def _get_config_from_pyproject_toml(key: str, filepath: Path) -> Any | None:
|
|
46
237
|
"""
|
|
47
|
-
|
|
48
|
-
|
|
238
|
+
Load a configuration value from the pyproject.toml [tool.jetbase] section.
|
|
239
|
+
|
|
49
240
|
Args:
|
|
50
|
-
|
|
241
|
+
key (str): The configuration key to retrieve.
|
|
242
|
+
filepath (Path): Path to the pyproject.toml file.
|
|
243
|
+
|
|
51
244
|
Returns:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
245
|
+
Any | None: The configuration value if found in the [tool.jetbase]
|
|
246
|
+
section, otherwise None.
|
|
247
|
+
"""
|
|
248
|
+
with open(filepath, "rb") as f:
|
|
249
|
+
pyproject_data = tomli.load(f)
|
|
250
|
+
|
|
251
|
+
config_value: Any = pyproject_data.get("tool", {}).get("jetbase", {}).get(key)
|
|
252
|
+
if config_value is None:
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
return config_value
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _find_pyproject_toml(start: Path | None = None) -> Path | None:
|
|
259
|
+
"""
|
|
260
|
+
Find pyproject.toml by traversing up from the starting directory.
|
|
261
|
+
|
|
262
|
+
Walks up the directory tree from the specified starting point until
|
|
263
|
+
it finds a pyproject.toml file or reaches the filesystem root.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
start (Path | None): The directory to start searching from.
|
|
267
|
+
Defaults to None, which uses the current working directory.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Path | None: The directory containing pyproject.toml if found,
|
|
271
|
+
otherwise None.
|
|
272
|
+
"""
|
|
273
|
+
if start is None:
|
|
274
|
+
start = Path.cwd()
|
|
275
|
+
|
|
276
|
+
current = start.resolve()
|
|
277
|
+
|
|
278
|
+
while True:
|
|
279
|
+
candidate = current / "pyproject.toml"
|
|
280
|
+
if candidate.exists():
|
|
281
|
+
return current
|
|
282
|
+
|
|
283
|
+
if current.parent == current: # reached root
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
current = current.parent
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _get_config_from_env_var(key: str) -> Any | None:
|
|
290
|
+
"""
|
|
291
|
+
Load a configuration value from a JETBASE_{KEY} environment variable.
|
|
292
|
+
|
|
293
|
+
Converts "true" and "false" string values to boolean True and False.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
key (str): The configuration key to retrieve. Will be converted
|
|
297
|
+
to uppercase and prefixed with "JETBASE_".
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Any | None: The environment variable value if set, otherwise None.
|
|
301
|
+
Boolean strings are converted to Python booleans.
|
|
302
|
+
"""
|
|
303
|
+
env_var_name = f"JETBASE_{key.upper()}"
|
|
304
|
+
config_value: str | None = os.getenv(env_var_name, None)
|
|
305
|
+
if config_value:
|
|
306
|
+
if config_value.lower() == "true":
|
|
307
|
+
return True
|
|
308
|
+
if config_value.lower() == "false":
|
|
309
|
+
return False
|
|
310
|
+
return config_value
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _get_config_help_message(key: str) -> str:
|
|
55
314
|
"""
|
|
315
|
+
Return a formatted help message for configuring a missing key.
|
|
316
|
+
|
|
317
|
+
Provides examples showing all the different methods available to
|
|
318
|
+
configure the specified key.
|
|
56
319
|
|
|
57
|
-
|
|
58
|
-
|
|
320
|
+
Args:
|
|
321
|
+
key (str): The configuration key that was not found.
|
|
59
322
|
|
|
60
|
-
|
|
323
|
+
Returns:
|
|
324
|
+
str: Multi-line help message with examples for env.py, environment
|
|
325
|
+
variables, jetbase.toml, and pyproject.toml configuration.
|
|
326
|
+
"""
|
|
327
|
+
env_var_name = f"JETBASE_{key.upper()}"
|
|
328
|
+
return (
|
|
329
|
+
f"Configuration '{key}' not found. Please configure it using one of these methods:\n\n"
|
|
330
|
+
f"1. jetbase/env.py file:\n"
|
|
331
|
+
f' {key} = "your_value"\n\n'
|
|
332
|
+
f"2. Environment variable:\n"
|
|
333
|
+
f' export {env_var_name}="your_value"\n\n'
|
|
334
|
+
f"3. jetbase.toml file:\n"
|
|
335
|
+
f' {key} = "your_value"\n\n'
|
|
336
|
+
f"4. pyproject.toml file:\n"
|
|
337
|
+
f" [tool.jetbase]\n"
|
|
338
|
+
f' {key} = "your_value"\n\n'
|
|
339
|
+
)
|
jetbase/constants.py
CHANGED
|
@@ -2,11 +2,19 @@ from typing import Final
|
|
|
2
2
|
|
|
3
3
|
BASE_DIR: Final[str] = "jetbase"
|
|
4
4
|
MIGRATIONS_DIR: Final[str] = "migrations"
|
|
5
|
-
|
|
5
|
+
ENV_FILE: Final[str] = "env.py"
|
|
6
|
+
RUNS_ALWAYS_FILE_PREFIX: Final[str] = "RA__"
|
|
7
|
+
RUNS_ON_CHANGE_FILE_PREFIX: Final[str] = "ROC__"
|
|
8
|
+
VERSION_FILE_PREFIX: Final[str] = "V"
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
ENV_FILE_CONTENT: Final[str] = """
|
|
8
11
|
# Jetbase Configuration
|
|
9
12
|
# Update the sqlalchemy_url with your database connection string.
|
|
10
13
|
|
|
11
14
|
sqlalchemy_url = "postgresql://user:password@localhost:5432/mydb"
|
|
12
15
|
"""
|
|
16
|
+
|
|
17
|
+
NEW_MIGRATION_FILE_CONTENT: Final[str] = """-- upgrade
|
|
18
|
+
|
|
19
|
+
-- rollback
|
|
20
|
+
"""
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
from typing import Generator
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import Connection, Engine, create_engine, text
|
|
5
|
+
|
|
6
|
+
from jetbase.config import get_config
|
|
7
|
+
from jetbase.database.queries.base import detect_db
|
|
8
|
+
from jetbase.enums import DatabaseType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@contextmanager
|
|
12
|
+
def get_db_connection() -> Generator[Connection, None, None]:
|
|
13
|
+
"""
|
|
14
|
+
Context manager that yields a database connection with a transaction.
|
|
15
|
+
|
|
16
|
+
Creates a database connection using the configured SQLAlchemy URL,
|
|
17
|
+
opens a transaction, and yields the connection. For PostgreSQL,
|
|
18
|
+
sets the search_path if a schema is configured.
|
|
19
|
+
|
|
20
|
+
Yields:
|
|
21
|
+
Connection: A SQLAlchemy Connection object within an active
|
|
22
|
+
transaction.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> with get_db_connection() as conn:
|
|
26
|
+
... conn.execute(query)
|
|
27
|
+
"""
|
|
28
|
+
sqlalchemy_url: str = get_config(required={"sqlalchemy_url"}).sqlalchemy_url
|
|
29
|
+
engine: Engine = create_engine(url=sqlalchemy_url)
|
|
30
|
+
db_type: DatabaseType = detect_db(sqlalchemy_url=sqlalchemy_url)
|
|
31
|
+
|
|
32
|
+
with engine.begin() as connection:
|
|
33
|
+
if db_type == DatabaseType.POSTGRESQL:
|
|
34
|
+
postgres_schema: str | None = get_config().postgres_schema
|
|
35
|
+
if postgres_schema:
|
|
36
|
+
connection.execute(
|
|
37
|
+
text("SET search_path TO :postgres_schema"),
|
|
38
|
+
parameters={"postgres_schema": postgres_schema},
|
|
39
|
+
)
|
|
40
|
+
yield connection
|