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.
Files changed (49) hide show
  1. jetbase/cli/main.py +146 -4
  2. jetbase/commands/current.py +20 -0
  3. jetbase/commands/fix_checksums.py +172 -0
  4. jetbase/commands/fix_files.py +133 -0
  5. jetbase/commands/history.py +53 -0
  6. jetbase/commands/init.py +29 -0
  7. jetbase/commands/lock_status.py +25 -0
  8. jetbase/commands/new.py +65 -0
  9. jetbase/commands/rollback.py +172 -0
  10. jetbase/commands/status.py +212 -0
  11. jetbase/commands/unlock.py +29 -0
  12. jetbase/commands/upgrade.py +248 -0
  13. jetbase/commands/validators.py +37 -0
  14. jetbase/config.py +304 -25
  15. jetbase/constants.py +10 -2
  16. jetbase/database/connection.py +40 -0
  17. jetbase/database/queries/base.py +353 -0
  18. jetbase/database/queries/default_queries.py +215 -0
  19. jetbase/database/queries/postgres.py +14 -0
  20. jetbase/database/queries/query_loader.py +87 -0
  21. jetbase/database/queries/sqlite.py +197 -0
  22. jetbase/engine/checksum.py +25 -0
  23. jetbase/engine/dry_run.py +105 -0
  24. jetbase/engine/file_parser.py +324 -0
  25. jetbase/engine/formatters.py +61 -0
  26. jetbase/engine/lock.py +65 -0
  27. jetbase/engine/repeatable.py +125 -0
  28. jetbase/engine/validation.py +238 -0
  29. jetbase/engine/version.py +144 -0
  30. jetbase/enums.py +37 -1
  31. jetbase/exceptions.py +87 -0
  32. jetbase/models.py +45 -0
  33. jetbase/repositories/lock_repo.py +129 -0
  34. jetbase/repositories/migrations_repo.py +451 -0
  35. jetbase-0.12.1.dist-info/METADATA +135 -0
  36. jetbase-0.12.1.dist-info/RECORD +39 -0
  37. {jetbase-0.7.0.dist-info → jetbase-0.12.1.dist-info}/WHEEL +1 -1
  38. jetbase/core/dry_run.py +0 -38
  39. jetbase/core/file_parser.py +0 -199
  40. jetbase/core/initialize.py +0 -33
  41. jetbase/core/repository.py +0 -169
  42. jetbase/core/rollback.py +0 -67
  43. jetbase/core/upgrade.py +0 -75
  44. jetbase/core/version.py +0 -163
  45. jetbase/queries.py +0 -72
  46. jetbase-0.7.0.dist-info/METADATA +0 -12
  47. jetbase-0.7.0.dist-info/RECORD +0 -17
  48. {jetbase-0.7.0.dist-info → jetbase-0.12.1.dist-info}/entry_points.txt +0 -0
  49. {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
- # from importlib import ModuleType
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
- from jetbase.constants import CONFIG_FILE
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 get_sqlalchemy_url(filename: str = CONFIG_FILE) -> str:
177
+ def _get_config_from_env_py(key: str, filepath: str = ENV_FILE) -> Any | None:
13
178
  """
14
- Load a config file and extract the sqlalchemy_url.
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
- config_filename: Name of the config file (e.g., "config.py")
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
- The sqlalchemy_url from the config file, or None if not found
189
+ Any | None: The configuration value if the attribute exists,
190
+ otherwise None.
21
191
  """
22
- config_path: str = os.path.join(os.getcwd(), filename)
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
- raw_sqlalchemy_url: Any | None = getattr(config, "sqlalchemy_url", None)
207
+ config_value: Any | None = getattr(config, key, None)
34
208
 
35
- if raw_sqlalchemy_url is None:
36
- raise AttributeError(
37
- f"'sqlalchemy_url' not found or is set to None. Please define it in {config_path}."
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
- sqlalchemy_url: str = _validate_sqlalchemy_url(url=raw_sqlalchemy_url)
228
+ with open(filepath, "rb") as f:
229
+ jetbase_data = tomli.load(f)
41
230
 
42
- return sqlalchemy_url
231
+ config_value: Any = jetbase_data.get(key, None)
43
232
 
233
+ return config_value
44
234
 
45
- def _validate_sqlalchemy_url(url: Any) -> str:
235
+
236
+ def _get_config_from_pyproject_toml(key: str, filepath: Path) -> Any | None:
46
237
  """
47
- Validates a SQLAlchemy URL string.
48
- This function checks if the provided URL is a valid string.
238
+ Load a configuration value from the pyproject.toml [tool.jetbase] section.
239
+
49
240
  Args:
50
- url (Any): The SQLAlchemy URL to validate (could be any type from user config).
241
+ key (str): The configuration key to retrieve.
242
+ filepath (Path): Path to the pyproject.toml file.
243
+
51
244
  Returns:
52
- str: The validated SQLAlchemy URL string.
53
- Raises:
54
- TypeError: If the provided URL is not a string.
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
- if not isinstance(url, str):
58
- raise TypeError(f"sqlalchemy_url must be a string, got {type(url).__name__}")
320
+ Args:
321
+ key (str): The configuration key that was not found.
59
322
 
60
- return url
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
- CONFIG_FILE: Final[str] = "config.py"
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
- CONFIG_FILE_CONTENT: Final[str] = """
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