rococo 1.2.0__tar.gz → 1.3.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.
Files changed (122) hide show
  1. {rococo-1.2.0 → rococo-1.3.0}/PKG-INFO +1 -1
  2. {rococo-1.2.0 → rococo-1.3.0}/rococo/data/postgresql.py +4 -2
  3. {rococo-1.2.0 → rococo-1.3.0}/rococo/data/surrealdb.py +4 -2
  4. {rococo-1.2.0 → rococo-1.3.0}/rococo/migrations/common/migration_runner.py +38 -42
  5. rococo-1.3.0/rococo/migrations/common/sql_migration_base.py +215 -0
  6. rococo-1.3.0/rococo/migrations/mysql/migration.py +40 -0
  7. rococo-1.3.0/rococo/migrations/postgres/migration.py +44 -0
  8. {rococo-1.2.0 → rococo-1.3.0}/rococo/repositories/postgresql/postgresql_repository.py +9 -4
  9. {rococo-1.2.0 → rococo-1.3.0}/rococo/repositories/surrealdb/surreal_db_repository.py +4 -2
  10. {rococo-1.2.0 → rococo-1.3.0}/rococo.egg-info/PKG-INFO +1 -1
  11. {rococo-1.2.0 → rococo-1.3.0}/rococo.egg-info/SOURCES.txt +3 -0
  12. {rococo-1.2.0 → rococo-1.3.0}/setup.py +1 -1
  13. rococo-1.3.0/tests/migration_cli_base_test.py +234 -0
  14. {rococo-1.2.0 → rococo-1.3.0}/tests/migration_runner_test.py +44 -0
  15. {rococo-1.2.0 → rococo-1.3.0}/tests/postgre_repository_test.py +6 -3
  16. rococo-1.3.0/tests/sql_migration_base_test.py +330 -0
  17. rococo-1.2.0/rococo/migrations/mysql/migration.py +0 -112
  18. rococo-1.2.0/rococo/migrations/postgres/migration.py +0 -111
  19. {rococo-1.2.0 → rococo-1.3.0}/LICENSE +0 -0
  20. {rococo-1.2.0 → rococo-1.3.0}/README.md +0 -0
  21. {rococo-1.2.0 → rococo-1.3.0}/rococo/__init__.py +0 -0
  22. {rococo-1.2.0 → rococo-1.3.0}/rococo/auth/__init__.py +0 -0
  23. {rococo-1.2.0 → rococo-1.3.0}/rococo/auth/tokens.py +0 -0
  24. {rococo-1.2.0 → rococo-1.3.0}/rococo/config/__init__.py +0 -0
  25. {rococo-1.2.0 → rococo-1.3.0}/rococo/config/config.py +0 -0
  26. {rococo-1.2.0 → rococo-1.3.0}/rococo/data/__init__.py +0 -0
  27. {rococo-1.2.0 → rococo-1.3.0}/rococo/data/base.py +0 -0
  28. {rococo-1.2.0 → rococo-1.3.0}/rococo/data/dynamodb.py +0 -0
  29. {rococo-1.2.0 → rococo-1.3.0}/rococo/data/mongodb.py +0 -0
  30. {rococo-1.2.0 → rococo-1.3.0}/rococo/data/mysql.py +0 -0
  31. {rococo-1.2.0 → rococo-1.3.0}/rococo/emailing/__init__.py +0 -0
  32. {rococo-1.2.0 → rococo-1.3.0}/rococo/emailing/base.py +0 -0
  33. {rococo-1.2.0 → rococo-1.3.0}/rococo/emailing/config.py +0 -0
  34. {rococo-1.2.0 → rococo-1.3.0}/rococo/emailing/enums.py +0 -0
  35. {rococo-1.2.0 → rococo-1.3.0}/rococo/emailing/factory.py +0 -0
  36. {rococo-1.2.0 → rococo-1.3.0}/rococo/emailing/mailjet.py +0 -0
  37. {rococo-1.2.0 → rococo-1.3.0}/rococo/emailing/ses.py +0 -0
  38. {rococo-1.2.0 → rococo-1.3.0}/rococo/faxing/__init__.py +0 -0
  39. {rococo-1.2.0 → rococo-1.3.0}/rococo/faxing/base.py +0 -0
  40. {rococo-1.2.0 → rococo-1.3.0}/rococo/faxing/config.py +0 -0
  41. {rococo-1.2.0 → rococo-1.3.0}/rococo/faxing/enums.py +0 -0
  42. {rococo-1.2.0 → rococo-1.3.0}/rococo/faxing/factory.py +0 -0
  43. {rococo-1.2.0 → rococo-1.3.0}/rococo/faxing/ifax.py +0 -0
  44. {rococo-1.2.0 → rococo-1.3.0}/rococo/messaging/__init__.py +0 -0
  45. {rococo-1.2.0 → rococo-1.3.0}/rococo/messaging/base.py +0 -0
  46. {rococo-1.2.0 → rococo-1.3.0}/rococo/messaging/rabbitmq.py +0 -0
  47. {rococo-1.2.0 → rococo-1.3.0}/rococo/messaging/sqs.py +0 -0
  48. {rococo-1.2.0 → rococo-1.3.0}/rococo/migrations/__init__.py +0 -0
  49. {rococo-1.2.0 → rococo-1.3.0}/rococo/migrations/common/__init__.py +0 -0
  50. {rococo-1.2.0 → rococo-1.3.0}/rococo/migrations/common/cli_base.py +0 -0
  51. {rococo-1.2.0 → rococo-1.3.0}/rococo/migrations/common/migration_base.py +0 -0
  52. {rococo-1.2.0 → rococo-1.3.0}/rococo/migrations/common/migration_template.py +0 -0
  53. {rococo-1.2.0 → rococo-1.3.0}/rococo/migrations/mongo/__init__.py +0 -0
  54. {rococo-1.2.0 → rococo-1.3.0}/rococo/migrations/mongo/cli.py +0 -0
  55. {rococo-1.2.0 → rococo-1.3.0}/rococo/migrations/mongo/migration.py +0 -0
  56. {rococo-1.2.0 → rococo-1.3.0}/rococo/migrations/mysql/__init__.py +0 -0
  57. {rococo-1.2.0 → rococo-1.3.0}/rococo/migrations/mysql/cli.py +0 -0
  58. {rococo-1.2.0 → rococo-1.3.0}/rococo/migrations/postgres/__init__.py +0 -0
  59. {rococo-1.2.0 → rococo-1.3.0}/rococo/migrations/postgres/cli.py +0 -0
  60. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/__init__.py +0 -0
  61. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/email.py +0 -0
  62. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/login_method.py +0 -0
  63. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/organization.py +0 -0
  64. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/otp_method.py +0 -0
  65. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/person.py +0 -0
  66. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/person_organization_role.py +0 -0
  67. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/recovery_code.py +0 -0
  68. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/surrealdb/__init__.py +0 -0
  69. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/surrealdb/email.py +0 -0
  70. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/surrealdb/login_method.py +0 -0
  71. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/surrealdb/organization.py +0 -0
  72. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/surrealdb/otp_method.py +0 -0
  73. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/surrealdb/person.py +0 -0
  74. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/surrealdb/person_organization_role.py +0 -0
  75. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/surrealdb/surreal_versioned_model.py +0 -0
  76. {rococo-1.2.0 → rococo-1.3.0}/rococo/models/versioned_model.py +0 -0
  77. {rococo-1.2.0 → rococo-1.3.0}/rococo/plugins/__init__.py +0 -0
  78. {rococo-1.2.0 → rococo-1.3.0}/rococo/plugins/pooled_connection.py +0 -0
  79. {rococo-1.2.0 → rococo-1.3.0}/rococo/repositories/__init__.py +0 -0
  80. {rococo-1.2.0 → rococo-1.3.0}/rococo/repositories/base_repository.py +0 -0
  81. {rococo-1.2.0 → rococo-1.3.0}/rococo/repositories/dynamodb/__init__.py +0 -0
  82. {rococo-1.2.0 → rococo-1.3.0}/rococo/repositories/dynamodb/dynamodb_repository.py +0 -0
  83. {rococo-1.2.0 → rococo-1.3.0}/rococo/repositories/mongodb/__init__.py +0 -0
  84. {rococo-1.2.0 → rococo-1.3.0}/rococo/repositories/mongodb/mongodb_repository.py +0 -0
  85. {rococo-1.2.0 → rococo-1.3.0}/rococo/repositories/mysql/__init__.py +0 -0
  86. {rococo-1.2.0 → rococo-1.3.0}/rococo/repositories/mysql/mysql_repository.py +0 -0
  87. {rococo-1.2.0 → rococo-1.3.0}/rococo/repositories/mysql/organization_repository.py +0 -0
  88. {rococo-1.2.0 → rococo-1.3.0}/rococo/repositories/postgresql/__init__.py +0 -0
  89. {rococo-1.2.0 → rococo-1.3.0}/rococo/repositories/surrealdb/__init__.py +0 -0
  90. {rococo-1.2.0 → rococo-1.3.0}/rococo/repositories/surrealdb/organization_repository.py +0 -0
  91. {rococo-1.2.0 → rococo-1.3.0}/rococo/repositories/surrealdb/person_organization_role_repository.py +0 -0
  92. {rococo-1.2.0 → rococo-1.3.0}/rococo/sms/__init__.py +0 -0
  93. {rococo-1.2.0 → rococo-1.3.0}/rococo/sms/base.py +0 -0
  94. {rococo-1.2.0 → rococo-1.3.0}/rococo/sms/config.py +0 -0
  95. {rococo-1.2.0 → rococo-1.3.0}/rococo/sms/enums.py +0 -0
  96. {rococo-1.2.0 → rococo-1.3.0}/rococo/sms/factory.py +0 -0
  97. {rococo-1.2.0 → rococo-1.3.0}/rococo/sms/twilio.py +0 -0
  98. {rococo-1.2.0 → rococo-1.3.0}/rococo.egg-info/dependency_links.txt +0 -0
  99. {rococo-1.2.0 → rococo-1.3.0}/rococo.egg-info/entry_points.txt +0 -0
  100. {rococo-1.2.0 → rococo-1.3.0}/rococo.egg-info/requires.txt +0 -0
  101. {rococo-1.2.0 → rococo-1.3.0}/rococo.egg-info/top_level.txt +0 -0
  102. {rococo-1.2.0 → rococo-1.3.0}/setup.cfg +0 -0
  103. {rococo-1.2.0 → rococo-1.3.0}/tests/__init__.py +0 -0
  104. {rococo-1.2.0 → rococo-1.3.0}/tests/base_repository_test.py +0 -0
  105. {rococo-1.2.0 → rococo-1.3.0}/tests/config_test.py +0 -0
  106. {rococo-1.2.0 → rococo-1.3.0}/tests/database-integration/__init__.py +0 -0
  107. {rococo-1.2.0 → rococo-1.3.0}/tests/database-integration/conftest.py +0 -0
  108. {rococo-1.2.0 → rococo-1.3.0}/tests/database-integration/test_dynamodb_integration.py +0 -0
  109. {rococo-1.2.0 → rococo-1.3.0}/tests/database-integration/test_models.py +0 -0
  110. {rococo-1.2.0 → rococo-1.3.0}/tests/database-integration/test_mongodb_integration.py +0 -0
  111. {rococo-1.2.0 → rococo-1.3.0}/tests/database-integration/test_mysql_integration.py +0 -0
  112. {rococo-1.2.0 → rococo-1.3.0}/tests/database-integration/test_postgres_integration.py +0 -0
  113. {rococo-1.2.0 → rococo-1.3.0}/tests/database-integration/test_surrealdb_integration.py +0 -0
  114. {rococo-1.2.0 → rococo-1.3.0}/tests/dynamodb_repository_test.py +0 -0
  115. {rococo-1.2.0 → rococo-1.3.0}/tests/model_test.py +0 -0
  116. {rococo-1.2.0 → rococo-1.3.0}/tests/mongodb_repository_test.py +0 -0
  117. {rococo-1.2.0 → rococo-1.3.0}/tests/mysql_repository_test.py +0 -0
  118. {rococo-1.2.0 → rococo-1.3.0}/tests/surrealdb_repository_test.py +0 -0
  119. {rococo-1.2.0 → rococo-1.3.0}/tests/test_calculated_properties_fix.py +0 -0
  120. {rococo-1.2.0 → rococo-1.3.0}/tests/test_faxing.py +0 -0
  121. {rococo-1.2.0 → rococo-1.3.0}/tests/test_mailjet_addresses.py +0 -0
  122. {rococo-1.2.0 → rococo-1.3.0}/tests/test_non_versioned_model.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rococo
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: A Python library to help build things the way we want them built
5
5
  Home-page: https://github.com/EcorRouge/rococo
6
6
  Author: Jay Grieves
@@ -160,7 +160,8 @@ class PostgreSQLAdapter(DbAdapter):
160
160
  conditions: Dict[str, Any],
161
161
  sort: List[Tuple[str, str]] = None,
162
162
  join_statements: list = None,
163
- additional_fields: list = None
163
+ additional_fields: list = None,
164
+ active: bool = True
164
165
  ) -> Optional[Dict[str, Any]]:
165
166
  fields = [f'{table}.*']
166
167
  if additional_fields:
@@ -175,7 +176,8 @@ class PostgreSQLAdapter(DbAdapter):
175
176
  if conditions:
176
177
  condition_strs_values = [self._build_condition_string(
177
178
  table, k, v) for k, v in conditions.items()]
178
- condition_strs_values.append((f"{table}.active = %s", ['true']))
179
+ if active:
180
+ condition_strs_values.append((f"{table}.active = %s", ['true']))
179
181
  query += f" WHERE {' AND '.join([condition_str for condition_str, condition_value in condition_strs_values])}"
180
182
 
181
183
  if sort:
@@ -108,7 +108,8 @@ class SurrealDbAdapter(DbAdapter):
108
108
  conditions: Dict[str, Any],
109
109
  sort: List[Tuple[str, str]] = None,
110
110
  fetch_related: list = None,
111
- additional_fields: list = None
111
+ additional_fields: list = None,
112
+ active: bool = True
112
113
  ) -> Dict[str, Any]:
113
114
  fields = ['*']
114
115
  if additional_fields:
@@ -120,7 +121,8 @@ class SurrealDbAdapter(DbAdapter):
120
121
  if conditions:
121
122
  condition_strs = [
122
123
  f"{self._build_condition_string(k, v)}" for k, v in conditions.items()]
123
- condition_strs.append("active=true")
124
+ if active:
125
+ condition_strs.append("active=true")
124
126
  query += f" WHERE {' AND '.join(condition_strs)}"
125
127
 
126
128
  if sort:
@@ -1,10 +1,8 @@
1
1
  import os
2
2
  import logging
3
+ import importlib
3
4
  from importlib import import_module
4
- # Assuming migration_template.py is in the same directory or accessible in python path
5
- # For a common module, it would typically be:
6
- # from rococo.migrations.common.migration_template import get_template
7
- from .migration_template import get_template # This relative import is fine if it's part of a package
5
+ from .migration_template import get_template
8
6
 
9
7
  class MigrationRunner:
10
8
  def __init__(self, migrations_dir, migration):
@@ -150,42 +148,40 @@ class MigrationRunner:
150
148
  print(f"Error creating migration file: {e}")
151
149
 
152
150
  def run_forward_migration_script(self, initial_db_version_for_run):
153
- # Renamed old_db_version to initial_db_version_for_run for clarity
154
- current_script_filename = self._get_forward_migration_script()
155
-
156
- if current_script_filename is None:
157
- latest_db_version_after_ops = self.get_db_version()
158
- if initial_db_version_for_run == latest_db_version_after_ops:
159
- # This means no scripts were found that could run from initial_db_version_for_run
160
- logging.info(f'No forward migration scripts found for current DB version: {initial_db_version_for_run}. Database is likely up to date.')
161
- else:
162
- # This means some migrations ran, and now no more are found.
163
- logging.info('All pending forward migrations complete!')
164
- logging.info(f'Latest DB version: {latest_db_version_after_ops}')
165
- return # End of migration run
166
-
167
- # Using os.path.basename to be platform-agnostic, though split('/') works on POSIX
168
- logging.info(f'Running forward migration: {os.path.basename(current_script_filename)}.py')
169
-
170
- try:
171
- # Dynamically import the migration module
172
- # Assumes migrations_dir is in sys.path (BaseCli handles this)
173
- module_name = current_script_filename
174
- migration_module = import_module(module_name) # Assumes migrations_dir is in sys.path
175
- migration_module.upgrade(self.migration)
176
- except Exception as e:
177
- logging.error(f"Error during forward migration {current_script_filename}.py: {e}", exc_info=True)
178
- # This print is for direct CLI feedback during a run
179
- print(f"Error applying migration {current_script_filename}.py. Halting.")
180
- # Potentially offer to show current DB version or suggest manual check
181
- print(f"Current DB version after error: {self.get_db_version()}")
182
- return # Stop further migrations
183
-
184
- # Recursively run next forward migration
185
- # The `initial_db_version_for_run` remains the version from the start of this `rf` command.
186
- # The next call to `_get_forward_migration_script` will use the *new* current DB version
187
- # (updated by the migration script that just ran).
188
- return self.run_forward_migration_script(initial_db_version_for_run)
151
+ # Iterates one migration at a time, re-resolving the next forward script
152
+ # against the DB version updated by the just-applied script. Iterative to
153
+ # avoid stack overflow on deep migration histories.
154
+ while True:
155
+ current_script_filename = self._get_forward_migration_script()
156
+
157
+ if current_script_filename is None:
158
+ latest_db_version_after_ops = self.get_db_version()
159
+ if initial_db_version_for_run == latest_db_version_after_ops:
160
+ logging.info(
161
+ f'No forward migration scripts found for current DB version: '
162
+ f'{initial_db_version_for_run}. Database is likely up to date.'
163
+ )
164
+ else:
165
+ logging.info('All pending forward migrations complete!')
166
+ logging.info(f'Latest DB version: {latest_db_version_after_ops}')
167
+ return
168
+
169
+ logging.info(
170
+ f'Running forward migration: {os.path.basename(current_script_filename)}.py'
171
+ )
172
+
173
+ try:
174
+ importlib.invalidate_caches()
175
+ migration_module = import_module(current_script_filename)
176
+ migration_module.upgrade(self.migration)
177
+ except Exception as e:
178
+ logging.error(
179
+ f"Error during forward migration {current_script_filename}.py: {e}",
180
+ exc_info=True,
181
+ )
182
+ print(f"Error applying migration {current_script_filename}.py. Halting.")
183
+ print(f"Current DB version after error: {self.get_db_version()}")
184
+ return
189
185
 
190
186
  def run_backward_migration_script(self):
191
187
  current_script_filename = self._get_backward_migration_script()
@@ -197,8 +193,8 @@ class MigrationRunner:
197
193
  logging.info(f'Running backward migration: {os.path.basename(current_script_filename)}.py')
198
194
 
199
195
  try:
200
- module_name = current_script_filename
201
- migration_module = import_module(module_name)
196
+ importlib.invalidate_caches()
197
+ migration_module = import_module(current_script_filename)
202
198
  migration_module.downgrade(self.migration)
203
199
  except Exception as e:
204
200
  logging.error(f"Error during backward migration {current_script_filename}.py: {e}", exc_info=True)
@@ -0,0 +1,215 @@
1
+ import logging
2
+ import re
3
+ from typing import Any, Iterable, Optional
4
+
5
+ from .migration_base import MigrationBase
6
+
7
+
8
+ class SQLMigrationBase(MigrationBase):
9
+ """Shared SQL migration helpers for relational adapters.
10
+
11
+ Concrete subclasses (e.g. PostgresMigration, MySQLMigration) provide the
12
+ dialect-specific SQL via class-level templates and queries. Identifiers
13
+ (table, column, index, constraint names) are validated against
14
+ ``_IDENT_RE`` before being interpolated into SQL. Values (database name,
15
+ version strings) are bound through the adapter's parameterized query
16
+ interface, never concatenated into SQL strings.
17
+ """
18
+
19
+ # Parameterized queries (use %s placeholders).
20
+ EXISTS_COLUMN_QUERY: str = NotImplemented # args: (database, table, column)
21
+ EXISTS_PK_QUERY: str = NotImplemented # args: (database, table)
22
+ UPDATE_VERSION_QUERY: str = NotImplemented # args: (version,)
23
+ INSERT_DB_VERSION_DATA_QUERY: str = NotImplemented # no args
24
+
25
+ # Identifier templates (use str.format with validated names).
26
+ DROP_PK_TEMPLATE: str = NotImplemented # {table}
27
+ ADD_PK_TEMPLATE: str = NotImplemented # {table}, {keys}
28
+ ADD_COLUMN_TEMPLATE: str = NotImplemented # {table}, {column}, {datatype}
29
+ DROP_COLUMN_TEMPLATE: str = NotImplemented # {table}, {column}
30
+ ADD_INDEX_TEMPLATE: str = NotImplemented # {table}, {index}, {column}
31
+ REMOVE_INDEX_TEMPLATE: str = NotImplemented # {table}, {index}
32
+ ALTER_INDEX_TEMPLATES: tuple = NotImplemented # tuple of templates run in order
33
+ ALTER_COLUMN_TEMPLATE: str = NotImplemented # {table}, {column}, {datatype}
34
+ RENAME_COLUMN_TEMPLATE: str = NotImplemented # {table}, {old}, {new}
35
+ RENAME_TABLE_TEMPLATE: str = NotImplemented # {old_table}, {new_table}
36
+ CREATE_TABLE_TEMPLATE: str = NotImplemented # {table}, {fields_with_type}
37
+ DROP_TABLE_IF_EXISTS_TEMPLATE: str = NotImplemented # {table}
38
+ DROP_TABLE_TEMPLATE: str = NotImplemented # {table}
39
+
40
+ _IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
41
+
42
+ def _validate_ident(self, name: str) -> str:
43
+ if not isinstance(name, str) or not self._IDENT_RE.match(name):
44
+ raise ValueError(f"invalid SQL identifier: {name!r}")
45
+ return name
46
+
47
+ def _validate_idents(self, names: Iterable[str]) -> None:
48
+ for n in names:
49
+ self._validate_ident(n)
50
+
51
+ def _validate_pk_keys(self, keys: str) -> str:
52
+ if not isinstance(keys, str) or not (keys.startswith("(") and keys.endswith(")")):
53
+ raise ValueError(
54
+ f"primary key keys must be wrapped in parentheses, got: {keys!r}"
55
+ )
56
+ inner = keys[1:-1]
57
+ for col in inner.split(","):
58
+ self._validate_ident(col.strip())
59
+ return keys
60
+
61
+ @staticmethod
62
+ def _extract_count(response: Any) -> int:
63
+ if not response:
64
+ return 0
65
+ row = response[0] if isinstance(response, list) else response
66
+ if not isinstance(row, dict) or not row:
67
+ return 0
68
+ value = next(iter(row.values()))
69
+ return int(value or 0)
70
+
71
+ def _does_column_exist(self, table_name, column_name, commit: bool = True) -> bool:
72
+ self._validate_idents([table_name, column_name])
73
+ rows = self.execute(
74
+ self.EXISTS_COLUMN_QUERY,
75
+ commit=commit,
76
+ args=(self.db_adapter._database, table_name, column_name),
77
+ )
78
+ return self._extract_count(rows) > 0
79
+
80
+ def _does_primary_key_constraint_exists(self, table_name, commit: bool = True) -> bool:
81
+ self._validate_ident(table_name)
82
+ rows = self.execute(
83
+ self.EXISTS_PK_QUERY,
84
+ commit=commit,
85
+ args=(self.db_adapter._database, table_name),
86
+ )
87
+ return self._extract_count(rows) > 0
88
+
89
+ def remove_primary_key(self, table_name, commit: bool = True):
90
+ self._validate_ident(table_name)
91
+ if self._does_primary_key_constraint_exists(table_name, commit=commit):
92
+ return self.execute(
93
+ self.DROP_PK_TEMPLATE.format(table=table_name), commit=commit
94
+ )
95
+ logging.info(
96
+ f"PRIMARY KEY constraint for table {table_name} does not exist. "
97
+ f"Skipping removal..."
98
+ )
99
+
100
+ def add_primary_key(self, table_name, keys: str, commit: bool = True):
101
+ self._validate_ident(table_name)
102
+ self._validate_pk_keys(keys)
103
+ if self._does_primary_key_constraint_exists(table_name, commit=commit):
104
+ self.execute(
105
+ self.DROP_PK_TEMPLATE.format(table=table_name), commit=commit
106
+ )
107
+ return self.execute(
108
+ self.ADD_PK_TEMPLATE.format(table=table_name, keys=keys), commit=commit
109
+ )
110
+
111
+ def add_column(self, table_name, column_name, datatype, commit: bool = True):
112
+ self._validate_idents([table_name, column_name])
113
+ if self._does_column_exist(table_name, column_name, commit=commit):
114
+ logging.info(
115
+ f"Column {column_name} for table {table_name} already exists. "
116
+ f"Skipping creation..."
117
+ )
118
+ return
119
+ query = self.ADD_COLUMN_TEMPLATE.format(
120
+ table=table_name, column=column_name, datatype=datatype
121
+ )
122
+ self.execute(query, commit=commit)
123
+
124
+ def drop_column(self, table_name, column_name, commit: bool = True):
125
+ self._validate_idents([table_name, column_name])
126
+ if not self._does_column_exist(table_name, column_name, commit=commit):
127
+ logging.info(
128
+ f"Column {column_name} for table {table_name} does not exist. "
129
+ f"Skipping deletion..."
130
+ )
131
+ return
132
+ query = self.DROP_COLUMN_TEMPLATE.format(
133
+ table=table_name, column=column_name
134
+ )
135
+ self.execute(query, commit=commit)
136
+
137
+ def alter_index(
138
+ self,
139
+ table_name,
140
+ new_index_name,
141
+ new_indexed_column,
142
+ old_index_name,
143
+ commit: bool = True,
144
+ ):
145
+ self._validate_idents(
146
+ [table_name, new_index_name, new_indexed_column, old_index_name]
147
+ )
148
+ for tmpl in self.ALTER_INDEX_TEMPLATES:
149
+ query = tmpl.format(
150
+ table=table_name,
151
+ new_index=new_index_name,
152
+ new_column=new_indexed_column,
153
+ old_index=old_index_name,
154
+ )
155
+ self.execute(query, commit=commit)
156
+
157
+ def add_index(self, table_name, index_name, indexed_column, commit: bool = True):
158
+ self._validate_idents([table_name, index_name, indexed_column])
159
+ query = self.ADD_INDEX_TEMPLATE.format(
160
+ table=table_name, index=index_name, column=indexed_column
161
+ )
162
+ self.execute(query, commit=commit)
163
+
164
+ def remove_index(self, table_name, index_name, commit: bool = True):
165
+ self._validate_idents([table_name, index_name])
166
+ query = self.REMOVE_INDEX_TEMPLATE.format(
167
+ table=table_name, index=index_name
168
+ )
169
+ self.execute(query, commit=commit)
170
+
171
+ def alter_column(self, table_name, column_name, datatype, commit: bool = True):
172
+ self._validate_idents([table_name, column_name])
173
+ query = self.ALTER_COLUMN_TEMPLATE.format(
174
+ table=table_name, column=column_name, datatype=datatype
175
+ )
176
+ self.execute(query, commit=commit)
177
+
178
+ def change_column_name(
179
+ self, table_name, old_column_name, new_column_name, commit: bool = True
180
+ ):
181
+ self._validate_idents([table_name, old_column_name, new_column_name])
182
+ query = self.RENAME_COLUMN_TEMPLATE.format(
183
+ table=table_name, old=old_column_name, new=new_column_name
184
+ )
185
+ self.execute(query, commit=commit)
186
+
187
+ def change_table_name(self, old_table, new_table, commit: bool = True):
188
+ self._validate_idents([old_table, new_table])
189
+ query = self.RENAME_TABLE_TEMPLATE.format(
190
+ old_table=old_table, new_table=new_table
191
+ )
192
+ self.execute(query, commit=commit)
193
+
194
+ def create_table(
195
+ self, table_name, fields_with_type, drop_if_exists: bool = True, commit: bool = True
196
+ ):
197
+ self._validate_ident(table_name)
198
+ if drop_if_exists:
199
+ drop_query = self.DROP_TABLE_IF_EXISTS_TEMPLATE.format(table=table_name)
200
+ self.execute(drop_query, commit=commit)
201
+ create_query = self.CREATE_TABLE_TEMPLATE.format(
202
+ table=table_name, fields_with_type=fields_with_type
203
+ )
204
+ self.execute(create_query, commit=commit)
205
+
206
+ def drop_table(self, table_name, commit: bool = True):
207
+ self._validate_ident(table_name)
208
+ query = self.DROP_TABLE_TEMPLATE.format(table=table_name)
209
+ self.execute(query, commit=commit)
210
+
211
+ def insert_db_version_data(self, commit: bool = True):
212
+ self.execute(self.INSERT_DB_VERSION_DATA_QUERY, commit=commit)
213
+
214
+ def update_version_table(self, version, commit: bool = True):
215
+ self.execute(self.UPDATE_VERSION_QUERY, commit=commit, args=(str(version),))
@@ -0,0 +1,40 @@
1
+ from ..common.sql_migration_base import SQLMigrationBase
2
+ from rococo.data.mysql import MySqlAdapter
3
+
4
+
5
+ class MySQLMigration(SQLMigrationBase):
6
+ EXISTS_COLUMN_QUERY = (
7
+ "SELECT COUNT(*) AS count "
8
+ "FROM information_schema.COLUMNS "
9
+ "WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s;"
10
+ )
11
+ EXISTS_PK_QUERY = (
12
+ "SELECT COUNT(*) AS count "
13
+ "FROM information_schema.TABLE_CONSTRAINTS "
14
+ "WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s "
15
+ "AND CONSTRAINT_TYPE = 'PRIMARY KEY';"
16
+ )
17
+ UPDATE_VERSION_QUERY = "UPDATE db_version SET version = %s;"
18
+ INSERT_DB_VERSION_DATA_QUERY = (
19
+ "INSERT INTO db_version (version) VALUES ('0000000000');"
20
+ )
21
+
22
+ DROP_PK_TEMPLATE = "ALTER TABLE {table} DROP PRIMARY KEY;"
23
+ ADD_PK_TEMPLATE = "ALTER TABLE {table} ADD PRIMARY KEY {keys};"
24
+ ADD_COLUMN_TEMPLATE = "ALTER TABLE {table} ADD {column} {datatype};"
25
+ DROP_COLUMN_TEMPLATE = "ALTER TABLE {table} DROP {column};"
26
+ ADD_INDEX_TEMPLATE = "ALTER TABLE {table} ADD INDEX {index} ({column});"
27
+ REMOVE_INDEX_TEMPLATE = "ALTER TABLE {table} DROP INDEX {index};"
28
+ ALTER_INDEX_TEMPLATES = (
29
+ "ALTER TABLE {table} ADD INDEX {new_index} ({new_column}), "
30
+ "DROP INDEX {old_index};",
31
+ )
32
+ ALTER_COLUMN_TEMPLATE = "ALTER TABLE {table} MODIFY COLUMN {column} {datatype};"
33
+ RENAME_COLUMN_TEMPLATE = "ALTER TABLE {table} RENAME COLUMN {old} TO {new};"
34
+ RENAME_TABLE_TEMPLATE = "ALTER TABLE {old_table} RENAME TO {new_table};"
35
+ CREATE_TABLE_TEMPLATE = "CREATE TABLE {table} ({fields_with_type});"
36
+ DROP_TABLE_IF_EXISTS_TEMPLATE = "DROP TABLE IF EXISTS {table};"
37
+ DROP_TABLE_TEMPLATE = "DROP TABLE {table};"
38
+
39
+ def __init__(self, db_adapter: MySqlAdapter):
40
+ super().__init__(db_adapter)
@@ -0,0 +1,44 @@
1
+ from ..common.sql_migration_base import SQLMigrationBase
2
+ from rococo.data.postgresql import PostgreSQLAdapter
3
+
4
+
5
+ class PostgresMigration(SQLMigrationBase):
6
+ EXISTS_COLUMN_QUERY = (
7
+ "SELECT COUNT(*) AS count "
8
+ "FROM information_schema.columns "
9
+ "WHERE table_catalog = %s AND table_name = %s AND column_name = %s;"
10
+ )
11
+ EXISTS_PK_QUERY = (
12
+ "SELECT COUNT(*) AS count "
13
+ "FROM information_schema.table_constraints "
14
+ "WHERE table_catalog = %s AND table_name = %s "
15
+ "AND constraint_type = 'PRIMARY KEY';"
16
+ )
17
+ UPDATE_VERSION_QUERY = "UPDATE db_version SET version = %s;"
18
+ INSERT_DB_VERSION_DATA_QUERY = (
19
+ "INSERT INTO db_version (version) VALUES ('0000000000');"
20
+ )
21
+
22
+ DROP_PK_TEMPLATE = (
23
+ "ALTER TABLE {table} DROP CONSTRAINT IF EXISTS {table}_pkey;"
24
+ )
25
+ ADD_PK_TEMPLATE = (
26
+ "ALTER TABLE {table} ADD CONSTRAINT {table}_pkey PRIMARY KEY {keys};"
27
+ )
28
+ ADD_COLUMN_TEMPLATE = "ALTER TABLE {table} ADD COLUMN {column} {datatype};"
29
+ DROP_COLUMN_TEMPLATE = "ALTER TABLE {table} DROP COLUMN {column};"
30
+ ADD_INDEX_TEMPLATE = "CREATE INDEX {index} ON {table} ({column});"
31
+ REMOVE_INDEX_TEMPLATE = "DROP INDEX IF EXISTS {index};"
32
+ ALTER_INDEX_TEMPLATES = (
33
+ "DROP INDEX IF EXISTS {old_index};",
34
+ "CREATE INDEX {new_index} ON {table} ({new_column});",
35
+ )
36
+ ALTER_COLUMN_TEMPLATE = "ALTER TABLE {table} ALTER COLUMN {column} TYPE {datatype};"
37
+ RENAME_COLUMN_TEMPLATE = "ALTER TABLE {table} RENAME COLUMN {old} TO {new};"
38
+ RENAME_TABLE_TEMPLATE = "ALTER TABLE {old_table} RENAME TO {new_table};"
39
+ CREATE_TABLE_TEMPLATE = "CREATE TABLE {table} ({fields_with_type});"
40
+ DROP_TABLE_IF_EXISTS_TEMPLATE = "DROP TABLE IF EXISTS {table} CASCADE;"
41
+ DROP_TABLE_TEMPLATE = "DROP TABLE {table} CASCADE;"
42
+
43
+ def __init__(self, db_adapter: PostgreSQLAdapter):
44
+ super().__init__(db_adapter)
@@ -79,7 +79,8 @@ class PostgreSQLRepository(BaseRepository):
79
79
  conditions = self._adjust_conditions(conditions)
80
80
 
81
81
  data = self._execute_within_context(
82
- self.adapter.get_one, self.table_name, conditions
82
+ self.adapter.get_one, self.table_name, conditions,
83
+ active=self._is_versioned_model()
83
84
  )
84
85
 
85
86
  if not data:
@@ -118,7 +119,8 @@ class PostgreSQLRepository(BaseRepository):
118
119
  conditions = self._adjust_conditions(conditions)
119
120
  # Fetch the records
120
121
  records = self._execute_within_context(
121
- self.adapter.get_many, self.table_name, conditions, sort, limit, offset
122
+ self.adapter.get_many, self.table_name, conditions, sort, limit, offset,
123
+ active=self._is_versioned_model()
122
124
  )
123
125
 
124
126
  # If the adapter returned a single dictionary, wrap it in a list
@@ -164,8 +166,11 @@ class PostgreSQLRepository(BaseRepository):
164
166
  int: The count of matching records.
165
167
  """
166
168
  # Prepare the actual conditions for the database query
167
- # Default conditions from BaseRepository
168
- db_conditions = {'latest': True, 'active': True}
169
+ # Only add versioning filters for VersionedModel
170
+ if self._is_versioned_model():
171
+ db_conditions = {'latest': True, 'active': True}
172
+ else:
173
+ db_conditions = {}
169
174
  if query: # Ensure query is not None before updating
170
175
  actual_query = self._adjust_conditions(
171
176
  query.copy()) # Adjust UUIDs in query
@@ -197,7 +197,8 @@ class SurrealDbRepository(BaseRepository):
197
197
  self.table_name,
198
198
  conditions,
199
199
  fetch_related=fetch_related,
200
- additional_fields=additional_fields
200
+ additional_fields=additional_fields,
201
+ active=self._is_versioned_model()
201
202
  )
202
203
  # prep model context
203
204
  self.model()
@@ -265,7 +266,8 @@ class SurrealDbRepository(BaseRepository):
265
266
  sort=sort,
266
267
  limit=limit,
267
268
  fetch_related=fetch_related,
268
- additional_fields=additional_fields
269
+ additional_fields=additional_fields,
270
+ active=self._is_versioned_model()
269
271
  )
270
272
  if isinstance(raw, dict):
271
273
  raw = [raw]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rococo
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: A Python library to help build things the way we want them built
5
5
  Home-page: https://github.com/EcorRouge/rococo
6
6
  Author: Jay Grieves
@@ -42,6 +42,7 @@ rococo/migrations/common/cli_base.py
42
42
  rococo/migrations/common/migration_base.py
43
43
  rococo/migrations/common/migration_runner.py
44
44
  rococo/migrations/common/migration_template.py
45
+ rococo/migrations/common/sql_migration_base.py
45
46
  rococo/migrations/mongo/__init__.py
46
47
  rococo/migrations/mongo/cli.py
47
48
  rococo/migrations/mongo/migration.py
@@ -95,11 +96,13 @@ tests/__init__.py
95
96
  tests/base_repository_test.py
96
97
  tests/config_test.py
97
98
  tests/dynamodb_repository_test.py
99
+ tests/migration_cli_base_test.py
98
100
  tests/migration_runner_test.py
99
101
  tests/model_test.py
100
102
  tests/mongodb_repository_test.py
101
103
  tests/mysql_repository_test.py
102
104
  tests/postgre_repository_test.py
105
+ tests/sql_migration_base_test.py
103
106
  tests/surrealdb_repository_test.py
104
107
  tests/test_calculated_properties_fix.py
105
108
  tests/test_faxing.py
@@ -64,7 +64,7 @@ extras_require["all"] = [
64
64
 
65
65
  setup(
66
66
  name='rococo',
67
- version='1.2.0',
67
+ version='1.3.0',
68
68
  packages=find_packages(),
69
69
  url='https://github.com/EcorRouge/rococo',
70
70
  license='MIT',