half-orm-dev 0.16.0a9__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 (58) hide show
  1. half_orm_dev/__init__.py +1 -0
  2. half_orm_dev/cli/__init__.py +9 -0
  3. half_orm_dev/cli/commands/__init__.py +56 -0
  4. half_orm_dev/cli/commands/apply.py +13 -0
  5. half_orm_dev/cli/commands/clone.py +102 -0
  6. half_orm_dev/cli/commands/init.py +331 -0
  7. half_orm_dev/cli/commands/new.py +15 -0
  8. half_orm_dev/cli/commands/patch.py +317 -0
  9. half_orm_dev/cli/commands/prepare.py +21 -0
  10. half_orm_dev/cli/commands/prepare_release.py +119 -0
  11. half_orm_dev/cli/commands/promote_to.py +127 -0
  12. half_orm_dev/cli/commands/release.py +344 -0
  13. half_orm_dev/cli/commands/restore.py +14 -0
  14. half_orm_dev/cli/commands/sync.py +13 -0
  15. half_orm_dev/cli/commands/todo.py +73 -0
  16. half_orm_dev/cli/commands/undo.py +17 -0
  17. half_orm_dev/cli/commands/update.py +73 -0
  18. half_orm_dev/cli/commands/upgrade.py +191 -0
  19. half_orm_dev/cli/main.py +103 -0
  20. half_orm_dev/cli_extension.py +38 -0
  21. half_orm_dev/database.py +1389 -0
  22. half_orm_dev/hgit.py +1025 -0
  23. half_orm_dev/hop.py +167 -0
  24. half_orm_dev/manifest.py +43 -0
  25. half_orm_dev/modules.py +456 -0
  26. half_orm_dev/patch.py +281 -0
  27. half_orm_dev/patch_manager.py +1694 -0
  28. half_orm_dev/patch_validator.py +335 -0
  29. half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +34 -0
  30. half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +2 -0
  31. half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +3 -0
  32. half_orm_dev/patches/log +2 -0
  33. half_orm_dev/patches/sql/half_orm_meta.sql +208 -0
  34. half_orm_dev/release_manager.py +2841 -0
  35. half_orm_dev/repo.py +1562 -0
  36. half_orm_dev/templates/.gitignore +15 -0
  37. half_orm_dev/templates/MANIFEST.in +1 -0
  38. half_orm_dev/templates/Pipfile +13 -0
  39. half_orm_dev/templates/README +25 -0
  40. half_orm_dev/templates/conftest_template +42 -0
  41. half_orm_dev/templates/init_module_template +10 -0
  42. half_orm_dev/templates/module_template_1 +12 -0
  43. half_orm_dev/templates/module_template_2 +6 -0
  44. half_orm_dev/templates/module_template_3 +3 -0
  45. half_orm_dev/templates/relation_test +23 -0
  46. half_orm_dev/templates/setup.py +81 -0
  47. half_orm_dev/templates/sql_adapter +9 -0
  48. half_orm_dev/templates/warning +12 -0
  49. half_orm_dev/utils.py +49 -0
  50. half_orm_dev/version.txt +1 -0
  51. half_orm_dev-0.16.0a9.dist-info/METADATA +935 -0
  52. half_orm_dev-0.16.0a9.dist-info/RECORD +58 -0
  53. half_orm_dev-0.16.0a9.dist-info/WHEEL +5 -0
  54. half_orm_dev-0.16.0a9.dist-info/licenses/AUTHORS +3 -0
  55. half_orm_dev-0.16.0a9.dist-info/licenses/LICENSE +14 -0
  56. half_orm_dev-0.16.0a9.dist-info/top_level.txt +2 -0
  57. tests/__init__.py +0 -0
  58. tests/conftest.py +329 -0
half_orm_dev/hop.py ADDED
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ Generates/Patches/Synchronizes a hop Python package with a PostgreSQL database
6
+ using the `hop` command.
7
+
8
+ Initiate a new project and repository with the `hop new <project_name>` command.
9
+ The <project_name> directory should not exist when using this command.
10
+
11
+ In the <project name> directory generated, the hop command helps you patch your
12
+ model, keep your Python synced with the PostgreSQL model, test your Python code and
13
+ deal with CI.
14
+
15
+ TODO:
16
+ On the 'devel' or any private branch hop applies patches if any, runs tests.
17
+ On the 'main' or 'master' branch, hop checks that your git repo is in sync with
18
+ the remote origin, synchronizes with devel branch if needed and tags your git
19
+ history with the last release applied.
20
+ """
21
+
22
+ import sys
23
+
24
+ import click
25
+
26
+ from half_orm_dev.repo import Repo
27
+ from half_orm import utils
28
+
29
+ class Hop:
30
+ "Sets the options available to the hop command"
31
+ __available_cmds = []
32
+ __command = None
33
+ def __init__(self):
34
+ self.__repo: Repo = Repo()
35
+ if not self.repo_checked:
36
+ Hop.__available_cmds = ['new']
37
+ else:
38
+ if not self.__repo.devel:
39
+ # Sync-only mode
40
+ Hop.__available_cmds = ['sync-package']
41
+ else:
42
+ # Full mode - check environment
43
+ if self.__repo.production:
44
+ Hop.__available_cmds = ['upgrade', 'restore']
45
+ else:
46
+ Hop.__available_cmds = ['prepare', 'apply', 'release', 'undo']
47
+ @property
48
+ def repo_checked(self):
49
+ "Returns wether we are in a repo or not."
50
+ return self.__repo.checked
51
+
52
+ @property
53
+ def model(self):
54
+ "Returns the model (half_orm.model.Model) associated to the repo."
55
+ return self.__repo.model
56
+
57
+ @property
58
+ def state(self):
59
+ "Returns the state of the repo."
60
+ return self.__repo.state
61
+
62
+ @property
63
+ def command(self):
64
+ "The command invoked (click)"
65
+ return self.__command
66
+
67
+ def add_commands(self, click_main):
68
+ "Adds the commands to the main click group."
69
+ @click.command()
70
+ @click.argument('package_name')
71
+ @click.option('-d', '--devel', is_flag=True, help="Development mode")
72
+ def new(package_name, devel=False):
73
+ """ Creates a new hop project named <package_name>.
74
+ """
75
+ self.__repo.init(package_name, devel)
76
+
77
+
78
+ @click.command()
79
+ @click.option(
80
+ '-l', '--level',
81
+ type=click.Choice(['patch', 'minor', 'major']), help="Release level.")
82
+ @click.option('-m', '--message', type=str, help="The git commit message")
83
+ def prepare(level, message=None):
84
+ """ Prepares the next release.
85
+ """
86
+ self.__command = 'prepare'
87
+ self.__repo.prepare_release(level, message)
88
+ sys.exit()
89
+
90
+ @click.command()
91
+ def apply():
92
+ """Apply the current release.
93
+ """
94
+ self.__command = 'apply'
95
+ self.__repo.apply_release()
96
+
97
+ @click.command()
98
+ @click.option(
99
+ '-d', '--database-only', is_flag=True,
100
+ help='Restore the database to the previous release.')
101
+ def undo(database_only):
102
+ """Undo the last release.
103
+ """
104
+ self.__command = 'undo'
105
+ self.__repo.undo_release(database_only)
106
+
107
+ @click.command()
108
+ # @click.option('-d', '--dry-run', is_flag=True, help='Do nothing')
109
+ # @click.option('-l', '--loop', is_flag=True, help='Run every patches to apply')
110
+ def upgrade():
111
+ """Apply one or many patches.
112
+
113
+ switches to hop_main, pulls should check the tags
114
+ """
115
+ self.__command = 'upgrade_prod'
116
+ self.__repo.upgrade_prod()
117
+
118
+ @click.command()
119
+ @click.argument('release')
120
+ def restore(release):
121
+ "Restore to release"
122
+ self.__repo.restore(release)
123
+
124
+ @click.command()
125
+ @click.option('-p', '--push', is_flag=True, help='Push git repo to origin')
126
+ def release(push=False):
127
+ self.__repo.commit_release(push)
128
+
129
+ @click.command()
130
+ def sync_package():
131
+ self.__repo.sync_package()
132
+
133
+ cmds = {
134
+ 'new': new,
135
+ 'prepare': prepare,
136
+ 'apply': apply,
137
+ 'undo': undo,
138
+ 'release': release,
139
+ 'sync-package': sync_package,
140
+ 'upgrade': upgrade,
141
+ 'restore': restore
142
+ }
143
+
144
+ for cmd in self.__available_cmds:
145
+ click_main.add_command(cmds[cmd])
146
+
147
+
148
+ hop = Hop()
149
+
150
+ @click.group(invoke_without_command=True)
151
+ @click.pass_context
152
+ def main(ctx):
153
+ """
154
+ Generates/Synchronises/Patches a python package from a PostgreSQL database
155
+ """
156
+ if hop.repo_checked and ctx.invoked_subcommand is None:
157
+ click.echo(hop.state)
158
+ elif not hop.repo_checked and ctx.invoked_subcommand != 'new':
159
+ click.echo(hop.state)
160
+ print(
161
+ "\nNot in a hop repository.\n"
162
+ f"Try {utils.Color.bold('hop new [--devel] <package name>')} or change directory.\n")
163
+
164
+ hop.add_commands(main)
165
+
166
+ if __name__ == '__main__':
167
+ main({})
@@ -0,0 +1,43 @@
1
+ """Manages the MANIFEST.json
2
+ """
3
+
4
+ import json
5
+ import os
6
+
7
+ from half_orm import utils
8
+
9
+ class Manifest:
10
+ "Manages the manifest of a release"
11
+ def __init__(self, path):
12
+ self.__hop_version = None
13
+ self.__changelog_msg = None
14
+ self.__file = os.path.join(path, 'MANIFEST.json')
15
+ if os.path.exists(self.__file):
16
+ manifest = utils.read(self.__file)
17
+ data = json.loads(manifest)
18
+ self.__hop_version = data['hop_version']
19
+ self.__changelog_msg = data['changelog_msg']
20
+
21
+ @property
22
+ def changelog_msg(self):
23
+ "Returns the changelog msg"
24
+ return self.__changelog_msg
25
+ @changelog_msg.setter
26
+ def changelog_msg(self, msg):
27
+ self.__changelog_msg = msg
28
+
29
+ @property
30
+ def hop_version(self):
31
+ "Returns the version of hop used to create this release"
32
+ return self.__hop_version
33
+ @hop_version.setter
34
+ def hop_version(self, release):
35
+ self.__hop_version = release
36
+
37
+ def write(self):
38
+ "Writes the manifest"
39
+ with open(self.__file, 'w', encoding='utf-8') as manifest:
40
+ manifest.write(json.dumps({
41
+ 'hop_version': self.__hop_version,
42
+ 'changelog_msg': self.__changelog_msg
43
+ }))
@@ -0,0 +1,456 @@
1
+ #!/usr/bin/env python3
2
+ #-*- coding: utf-8 -*-
3
+ # pylint: disable=invalid-name, protected-access
4
+
5
+ """
6
+ Generates/Patches/Synchronizes a hop Python package with a PostgreSQL database
7
+ with the `hop` command.
8
+
9
+ Initiate a new project and repository with the `hop create <project_name>` command.
10
+ The <project_name> directory should not exist when using this command.
11
+
12
+ In the dbname directory generated, the hop command helps you patch, test and
13
+ deal with CI.
14
+
15
+ TODO:
16
+ On the 'devel' or any private branch hop applies patches if any, runs tests.
17
+ On the 'main' or 'master' branch, hop checks that your git repo is in sync with
18
+ the remote origin, synchronizes with devel branch if needed and tags your git
19
+ history with the last release applied.
20
+ """
21
+
22
+ import importlib
23
+ import os
24
+ import re
25
+ import shutil
26
+ import sys
27
+ import time
28
+ from keyword import iskeyword
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+ from half_orm.pg_meta import camel_case
33
+ from half_orm.model_errors import UnknownRelation
34
+ from half_orm.sql_adapter import SQL_ADAPTER
35
+
36
+ from half_orm import utils
37
+ from .utils import TEMPLATE_DIRS, hop_version
38
+
39
+ def read_template(file_name):
40
+ "helper"
41
+ with open(os.path.join(TEMPLATE_DIRS, file_name), encoding='utf-8') as file_:
42
+ return file_.read()
43
+
44
+ NO_APAPTER = {}
45
+ HO_DATACLASSES = [
46
+ '''import dataclasses
47
+ from half_orm.relation import DC_Relation
48
+ from half_orm.field import Field''']
49
+ HO_DATACLASSES_IMPORTS = set()
50
+ INIT_MODULE_TEMPLATE = read_template('init_module_template')
51
+ MODULE_TEMPLATE_1 = read_template('module_template_1')
52
+ MODULE_TEMPLATE_2 = read_template('module_template_2')
53
+ MODULE_TEMPLATE_3 = read_template('module_template_3')
54
+ WARNING_TEMPLATE = read_template('warning')
55
+ CONFTEST = read_template('conftest_template')
56
+ TEST = read_template('relation_test')
57
+ SQL_ADAPTER_TEMPLATE = read_template('sql_adapter')
58
+ SKIP = re.compile('[A-Z]')
59
+
60
+ MODULE_FORMAT = (
61
+ "{rt1}" +
62
+ "{bc_}{global_user_s_code}{ec_}" +
63
+ "{rt2}" +
64
+ " {bc_}{user_s_class_attr} {ec_}" +
65
+ "{rt3}\n " +
66
+ "{bc_}{user_s_code}")
67
+ AP_EPILOG = """"""
68
+ INIT_PY = '__init__.py'
69
+ CONFTEST_PY = 'conftest.py'
70
+ DO_NOT_REMOVE = [INIT_PY]
71
+ TEST_PREFIX = 'test_'
72
+ TEST_SUFFIX = '.py'
73
+
74
+ MODEL = None
75
+
76
+
77
+ def __get_test_directory_path(schema_name, table_name, base_dir):
78
+ """
79
+ Calculate the test directory path for a given schema and table.
80
+
81
+ Args:
82
+ schema_name: PostgreSQL schema name (e.g., 'public')
83
+ table_name: PostgreSQL table name (e.g., 'user_profiles')
84
+ base_dir: Project base directory path
85
+
86
+ Returns:
87
+ Path: tests/schema_name/table_name/
88
+
89
+ Example:
90
+ __get_test_directory_path('public', 'user_profiles', '/path/to/project')
91
+ # Returns: Path('/path/to/project/tests/public/user_profiles')
92
+ """
93
+ base_path = Path(base_dir)
94
+ tests_dir = base_path / 'tests'
95
+
96
+ # Convert schema name: dots to underscores, keep original underscores
97
+ schema_dir_name = schema_name.replace('.', '_')
98
+
99
+ # Table name: keep underscores as-is
100
+ table_dir_name = table_name
101
+
102
+ return tests_dir / schema_dir_name / table_dir_name
103
+
104
+
105
+ def __get_test_file_path(schema_name, table_name, base_dir, package_name):
106
+ """
107
+ Calculate the complete test file path for a given schema and table.
108
+
109
+ Args:
110
+ schema_name: PostgreSQL schema name (e.g., 'public')
111
+ table_name: PostgreSQL table name (e.g., 'user_profiles')
112
+ base_dir: Project base directory path
113
+ package_name: Python package name
114
+
115
+ Returns:
116
+ Path: Complete path to test file
117
+
118
+ Example:
119
+ __get_test_file_path('public', 'user_profiles', '/path', 'mydb')
120
+ # Returns: Path('/path/tests/public/user_profiles/test_public_user_profiles.py')
121
+ """
122
+ test_dir = __get_test_directory_path(schema_name, table_name, base_dir)
123
+
124
+ # Convert schema and table names for filename
125
+ schema_file_name = schema_name.replace('.', '_')
126
+ table_file_name = table_name
127
+
128
+ # Construct filename: test_<schema>_<table>.py
129
+ test_filename = f"{TEST_PREFIX}{schema_file_name}_{table_file_name}{TEST_SUFFIX}"
130
+
131
+ return test_dir / test_filename
132
+
133
+
134
+ def __get_full_class_name(schemaname, relationname):
135
+ schemaname = ''.join([elt.capitalize() for elt in schemaname.split('.')])
136
+ relationname = ''.join([elt.capitalize() for elt in relationname.split('_')])
137
+ return f'{schemaname}{relationname}'
138
+
139
+
140
+ def __get_field_desc(field_name, field):
141
+ #TODO: REFACTOR
142
+ sql_type = field._metadata['fieldtype']
143
+ field_desc = SQL_ADAPTER.get(sql_type)
144
+ if field_desc is None:
145
+ if not NO_APAPTER.get(sql_type):
146
+ NO_APAPTER[sql_type] = 0
147
+ NO_APAPTER[sql_type] += 1
148
+ field_desc = Any
149
+ if field_desc.__module__ != 'builtins':
150
+ HO_DATACLASSES_IMPORTS.add(field_desc.__module__)
151
+ ext = 'Any'
152
+ if hasattr(field_desc, '__name__'):
153
+ ext = field_desc.__name__
154
+ field_desc = f'{field_desc.__module__}.{ext}'
155
+ else:
156
+ field_desc = field_desc.__name__
157
+ value = 'dataclasses.field(default=None)'
158
+ if field._metadata['fieldtype'][0] == '_':
159
+ value = 'dataclasses.field(default_factory=list)'
160
+ field_desc = f'{field_desc} = {value}'
161
+ field_desc = f" {field_name}: {field_desc}"
162
+ error = utils.check_attribute_name(field_name)
163
+ if error:
164
+ field_desc = f'# {field_desc} FIX ME! {error}'
165
+ return field_desc
166
+
167
+
168
+ def __gen_dataclass(relation, fkeys):
169
+ rel = relation()
170
+ dc_name = relation._ho_dataclass_name()
171
+ fields = []
172
+ post_init = [' def __post_init__(self):']
173
+ for field_name, field in rel._ho_fields.items():
174
+ fields.append(__get_field_desc(field_name, field))
175
+ post_init.append(f' self.{field_name}: Field = None')
176
+
177
+ fkeys = {value:key for key, value in fkeys.items() if key != ''}
178
+ for key, value in rel()._ho_fkeys.items():
179
+ if key in fkeys:
180
+ fkey_alias = fkeys[key]
181
+ fdc_name = f'{value._FKey__relation._ho_dataclass_name()}'
182
+ post_init.append(f" self.{fkey_alias} = {fdc_name}")
183
+ return '\n'.join([f'@dataclasses.dataclass\nclass {dc_name}(DC_Relation):'] + fields + post_init)
184
+
185
+
186
+ def __get_modules_list(dir, files_list, files):
187
+ all_ = []
188
+ for file_ in files:
189
+ if re.findall(SKIP, file_):
190
+ continue
191
+ path_ = os.path.join(dir, file_)
192
+ if path_ not in files_list and file_ not in DO_NOT_REMOVE:
193
+ # Filter out both old and new test file patterns
194
+ if (path_.find('__pycache__') == -1 and
195
+ not file_.endswith('_test.py') and
196
+ not file_.startswith('test_')):
197
+ print(f"REMOVING: {path_}")
198
+ os.remove(path_)
199
+ continue
200
+ if (re.findall('.py$', file_) and
201
+ file_ != INIT_PY and
202
+ file_ != '__pycache__' and
203
+ not file_.endswith('_test.py') and
204
+ not file_.startswith('test_')):
205
+ all_.append(file_.replace('.py', ''))
206
+ all_.sort()
207
+ return all_
208
+
209
+
210
+ def __update_init_files(package_dir, files_list, warning):
211
+ """Update __all__ lists in __init__ files.
212
+ """
213
+ for dir, _, files in os.walk(package_dir):
214
+ if dir == package_dir:
215
+ continue
216
+ reldir = dir.replace(package_dir, '')
217
+ if re.findall(SKIP, reldir):
218
+ continue
219
+ all_ = __get_modules_list(dir, files_list, files)
220
+ dirs = next(os.walk(dir))[1]
221
+
222
+ if len(all_) == 0 and dirs == ['__pycache__']:
223
+ shutil.rmtree(dir)
224
+ else:
225
+ with open(os.path.join(dir, INIT_PY), 'w', encoding='utf-8') as init_file:
226
+ init_file.write(f'"""{warning}"""\n\n')
227
+ all_ = ",\n ".join([f"'{elt}'" for elt in all_])
228
+ init_file.write(f'__all__ = [\n {all_}\n]\n')
229
+
230
+
231
+ def __get_inheritance_info(rel, package_name):
232
+ """Returns inheritance informations for the rel relation.
233
+ """
234
+ inheritance_import_list = []
235
+ inherited_classes_aliases_list = []
236
+ for base in rel.__class__.__bases__:
237
+ if base.__name__ != 'Relation' and hasattr(base, '_t_fqrn'):
238
+ inh_sfqrn = list(base._t_fqrn)
239
+ inh_sfqrn[0] = package_name
240
+ inh_cl_alias = f"{camel_case(inh_sfqrn[1])}{camel_case(inh_sfqrn[2])}"
241
+ inh_cl_name = f"{camel_case(inh_sfqrn[2])}"
242
+ from_import = f"from {'.'.join(inh_sfqrn)} import {inh_cl_name} as {inh_cl_alias}"
243
+ inheritance_import_list.append(from_import)
244
+ inherited_classes_aliases_list.append(inh_cl_alias)
245
+ inheritance_import = "\n".join(inheritance_import_list)
246
+ inherited_classes = ", ".join(inherited_classes_aliases_list)
247
+ if inherited_classes.strip():
248
+ inherited_classes = f"{inherited_classes}, "
249
+ return inheritance_import, inherited_classes
250
+
251
+
252
+ def __get_fkeys(repo, class_name, module_path):
253
+ try:
254
+ mod_path = module_path.replace(repo.base_dir, '').replace(os.path.sep, '.')[1:-3]
255
+ mod = importlib.import_module(mod_path)
256
+ importlib.reload(mod)
257
+ cls = mod.__dict__[class_name]
258
+ fkeys = cls.__dict__.get('Fkeys', {})
259
+ return fkeys
260
+ except ModuleNotFoundError:
261
+ pass
262
+ return {}
263
+
264
+
265
+ def __assemble_module_template(module_path):
266
+ """Construct the module after slicing it if it already exists.
267
+ """
268
+ ALT_BEGIN_CODE = "#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!\n"
269
+ user_s_code = ""
270
+ global_user_s_code = "\n"
271
+ module_template = MODULE_FORMAT
272
+ user_s_class_attr = ''
273
+ if os.path.exists(module_path):
274
+ module_code = utils.read(module_path)
275
+ if module_code.find(ALT_BEGIN_CODE) != -1:
276
+ module_code = module_code.replace(ALT_BEGIN_CODE, utils.BEGIN_CODE)
277
+ user_s_code = module_code.rsplit(utils.BEGIN_CODE, 1)[1]
278
+ user_s_code = user_s_code.replace('{', '{{').replace('}', '}}')
279
+ global_user_s_code = module_code.rsplit(utils.END_CODE)[0].split(utils.BEGIN_CODE)[1]
280
+ global_user_s_code = global_user_s_code.replace('{', '{{').replace('}', '}}')
281
+ user_s_class_attr = module_code.split(utils.BEGIN_CODE)[2].split(f' {utils.END_CODE}')[0]
282
+ user_s_class_attr = user_s_class_attr.replace('{', '{{').replace('}', '}}')
283
+ return module_template.format(
284
+ rt1=MODULE_TEMPLATE_1, rt2=MODULE_TEMPLATE_2, rt3=MODULE_TEMPLATE_3,
285
+ bc_=utils.BEGIN_CODE, ec_=utils.END_CODE,
286
+ global_user_s_code=global_user_s_code,
287
+ user_s_class_attr=user_s_class_attr,
288
+ user_s_code=user_s_code)
289
+
290
+
291
+ def __update_this_module(
292
+ repo, relation, package_dir, package_name):
293
+ """Updates the module and generates corresponding test file."""
294
+ _, fqtn = relation
295
+ path = list(fqtn)
296
+ if path[1].find('half_orm_meta') == 0:
297
+ # hop internal. do nothing
298
+ return None
299
+ fqtn = '.'.join(path[1:])
300
+ try:
301
+ rel = repo.database.model.get_relation_class(fqtn)()
302
+ except (TypeError, UnknownRelation) as err:
303
+ sys.stderr.write(f"{err}\n{fqtn}\n")
304
+ sys.stderr.flush()
305
+ return None
306
+
307
+ fields = []
308
+ kwargs = []
309
+ arg_names = []
310
+ for key, value in rel._ho_fields.items():
311
+ error = utils.check_attribute_name(key)
312
+ if not error:
313
+ fields.append(f"self.{key}: Field = None")
314
+ kwarg_type = 'typing.Any'
315
+ if hasattr(value.py_type, '__name__'):
316
+ kwarg_type = str(value.py_type.__name__)
317
+ kwargs.append(f"{key}: '{kwarg_type}'=None")
318
+ arg_names.append(f'{key}={key}')
319
+ fields = "\n ".join(fields)
320
+ kwargs.append('**kwargs')
321
+ kwargs = ", ".join(kwargs)
322
+ arg_names = ", ".join(arg_names)
323
+
324
+ path[0] = package_dir
325
+ path[1] = path[1].replace('.', os.sep)
326
+
327
+ path = [iskeyword(elt) and f'{elt}_' or elt for elt in path]
328
+ class_name = camel_case(path[-1])
329
+ module_path = f"{os.path.join(*path)}.py"
330
+ path_1 = os.path.join(*path[:-1])
331
+ if not os.path.exists(path_1):
332
+ os.makedirs(path_1)
333
+
334
+ module_template = __assemble_module_template(module_path)
335
+ inheritance_import, inherited_classes = __get_inheritance_info(
336
+ rel, package_name)
337
+
338
+ # Generate Python module
339
+ with open(module_path, 'w', encoding='utf-8') as file_:
340
+ documentation = "\n".join([line and f" {line}" or "" for line in str(rel).split("\n")])
341
+ file_.write(
342
+ module_template.format(
343
+ hop_release = hop_version(),
344
+ module=f"{package_name}.{fqtn}",
345
+ package_name=package_name,
346
+ documentation=documentation,
347
+ inheritance_import=inheritance_import,
348
+ inherited_classes=inherited_classes,
349
+ class_name=class_name,
350
+ dc_name=rel._ho_dataclass_name(),
351
+ fqtn=fqtn,
352
+ kwargs=kwargs,
353
+ arg_names=arg_names,
354
+ warning=WARNING_TEMPLATE.format(package_name=package_name)))
355
+
356
+ # Generate test file in tests/ directory structure
357
+ schema_name = path[1].replace(os.sep, '.') # Convert back to schema.name format
358
+ table_name = path[-1]
359
+ test_file_path = __get_test_file_path(schema_name, table_name, repo.base_dir, package_name)
360
+
361
+ if not test_file_path.exists():
362
+ # Create test directory structure
363
+ test_file_path.parent.mkdir(parents=True, exist_ok=True)
364
+
365
+ # Generate test file
366
+ with open(test_file_path, 'w', encoding='utf-8') as file_:
367
+ file_.write(TEST.format(
368
+ package_name=package_name,
369
+ module=f"{package_name}.{fqtn}",
370
+ class_name=class_name))
371
+
372
+ HO_DATACLASSES.append(__gen_dataclass(
373
+ rel, __get_fkeys(repo, class_name, module_path)))
374
+
375
+ return module_path
376
+
377
+
378
+ def __reset_dataclasses(repo, package_dir):
379
+ with open(os.path.join(package_dir, "ho_dataclasses.py"), "w", encoding='utf-8') as file_:
380
+ for relation in repo.database.model._relations():
381
+ t_qrn = relation[1][1:]
382
+ if t_qrn[0].find('half_orm') == 0:
383
+ continue
384
+ file_.write(f'class DC_{__get_full_class_name(*t_qrn)}: ...\n')
385
+
386
+
387
+ def __gen_dataclasses(package_dir, package_name):
388
+ with open(os.path.join(package_dir, "ho_dataclasses.py"), "w", encoding='utf-8') as file_:
389
+ file_.write(f"# dataclasses for {package_name}\n\n")
390
+ hd_imports = list(HO_DATACLASSES_IMPORTS)
391
+ hd_imports.sort()
392
+ for to_import in hd_imports:
393
+ file_.write(f"import {to_import}\n")
394
+ file_.write("\n")
395
+ for dc in HO_DATACLASSES:
396
+ file_.write(f"\n{dc}\n")
397
+
398
+
399
+ def generate(repo):
400
+ """Synchronize the modules with the structure of the relation in PG."""
401
+ package_name = repo.name
402
+ base_dir = Path(repo.base_dir)
403
+ package_dir = base_dir / package_name
404
+ files_list = []
405
+
406
+ try:
407
+ sql_adapter_module = importlib.import_module('.sql_adapter', package_name)
408
+ SQL_ADAPTER.update(sql_adapter_module.SQL_ADAPTER)
409
+ except ModuleNotFoundError as exc:
410
+ package_dir.mkdir(parents=True, exist_ok=True)
411
+ with open(package_dir / 'sql_adapter.py', "w", encoding='utf-8') as file_:
412
+ file_.write(SQL_ADAPTER_TEMPLATE)
413
+ sys.stderr.write(f"{exc}\n")
414
+ except AttributeError as exc:
415
+ sys.stderr.write(f"{exc}\n")
416
+
417
+ repo.database.model._reload()
418
+
419
+ if not package_dir.exists():
420
+ package_dir.mkdir(parents=True)
421
+
422
+ __reset_dataclasses(repo, str(package_dir))
423
+
424
+ # Generate package __init__.py
425
+ with open(package_dir / INIT_PY, 'w', encoding='utf-8') as file_:
426
+ file_.write(INIT_MODULE_TEMPLATE.format(package_name=package_name))
427
+
428
+ # Generate tests/conftest.py instead of package/base_test.py
429
+ tests_dir = base_dir / 'tests'
430
+ tests_dir.mkdir(exist_ok=True)
431
+
432
+ conftest_path = tests_dir / CONFTEST_PY
433
+ if not conftest_path.exists():
434
+ with open(conftest_path, 'w', encoding='utf-8') as file_:
435
+ file_.write(CONFTEST.format(
436
+ package_name=package_name,
437
+ hop_release=hop_version()))
438
+
439
+ warning = WARNING_TEMPLATE.format(package_name=package_name)
440
+
441
+ # Generate modules for each relation
442
+ for relation in repo.database.model._relations():
443
+ module_path = __update_this_module(repo, relation, str(package_dir), package_name)
444
+ if module_path:
445
+ files_list.append(module_path)
446
+ # Tests are no longer added to files_list (they live in tests/ directory)
447
+
448
+ __gen_dataclasses(str(package_dir), package_name)
449
+
450
+ if len(NO_APAPTER):
451
+ print("MISSING ADAPTER FOR SQL TYPE")
452
+ print(f"Add the following items to __SQL_ADAPTER in {package_dir / 'sql_adapter.py'}")
453
+ for key in NO_APAPTER.keys():
454
+ print(f" '{key}': typing.Any,")
455
+
456
+ __update_init_files(str(package_dir), files_list, warning)