half-orm-dev 0.16.0a1__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 (38) hide show
  1. half_orm_dev/__init__.py +0 -0
  2. half_orm_dev/changelog.py +117 -0
  3. half_orm_dev/cli_extension.py +171 -0
  4. half_orm_dev/database.py +127 -0
  5. half_orm_dev/db_conn.py +134 -0
  6. half_orm_dev/hgit.py +202 -0
  7. half_orm_dev/hop.py +167 -0
  8. half_orm_dev/manifest.py +43 -0
  9. half_orm_dev/modules.py +357 -0
  10. half_orm_dev/patch.py +348 -0
  11. half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +34 -0
  12. half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +2 -0
  13. half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +3 -0
  14. half_orm_dev/patches/log +2 -0
  15. half_orm_dev/patches/sql/half_orm_meta.sql +208 -0
  16. half_orm_dev/repo.py +266 -0
  17. half_orm_dev/templates/.gitignore +14 -0
  18. half_orm_dev/templates/MANIFEST.in +1 -0
  19. half_orm_dev/templates/Pipfile +13 -0
  20. half_orm_dev/templates/README +25 -0
  21. half_orm_dev/templates/base_test +26 -0
  22. half_orm_dev/templates/init_module_template +6 -0
  23. half_orm_dev/templates/module_template_1 +12 -0
  24. half_orm_dev/templates/module_template_2 +5 -0
  25. half_orm_dev/templates/module_template_3 +3 -0
  26. half_orm_dev/templates/relation_test +19 -0
  27. half_orm_dev/templates/setup.py +81 -0
  28. half_orm_dev/templates/sql_adapter +9 -0
  29. half_orm_dev/templates/warning +12 -0
  30. half_orm_dev/utils.py +12 -0
  31. half_orm_dev/version.txt +1 -0
  32. half_orm_dev-0.16.0a1.dist-info/METADATA +314 -0
  33. half_orm_dev-0.16.0a1.dist-info/RECORD +38 -0
  34. half_orm_dev-0.16.0a1.dist-info/WHEEL +5 -0
  35. half_orm_dev-0.16.0a1.dist-info/entry_points.txt +2 -0
  36. half_orm_dev-0.16.0a1.dist-info/licenses/AUTHORS +3 -0
  37. half_orm_dev-0.16.0a1.dist-info/licenses/LICENSE +14 -0
  38. half_orm_dev-0.16.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,357 @@
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 typing import Any
30
+
31
+ from half_orm.pg_meta import camel_case
32
+ from half_orm.model_errors import UnknownRelation
33
+ from half_orm.sql_adapter import SQL_ADAPTER
34
+
35
+ from half_orm import utils
36
+ from .utils import TEMPLATE_DIRS, hop_version
37
+
38
+ def read_template(file_name):
39
+ "helper"
40
+ with open(os.path.join(TEMPLATE_DIRS, file_name), encoding='utf-8') as file_:
41
+ return file_.read()
42
+
43
+ NO_APAPTER = {}
44
+ HO_DATACLASSES = [
45
+ '''import dataclasses
46
+ from half_orm.relation import DC_Relation
47
+ from half_orm.field import Field''']
48
+ HO_DATACLASSES_IMPORTS = set()
49
+ INIT_MODULE_TEMPLATE = read_template('init_module_template')
50
+ MODULE_TEMPLATE_1 = read_template('module_template_1')
51
+ MODULE_TEMPLATE_2 = read_template('module_template_2')
52
+ MODULE_TEMPLATE_3 = read_template('module_template_3')
53
+ WARNING_TEMPLATE = read_template('warning')
54
+ BASE_TEST = read_template('base_test')
55
+ TEST = read_template('relation_test')
56
+ SQL_ADAPTER_TEMPLATE = read_template('sql_adapter')
57
+ SKIP = re.compile('[A-Z]')
58
+
59
+ MODULE_FORMAT = (
60
+ "{rt1}" +
61
+ "{bc_}{global_user_s_code}{ec_}" +
62
+ "{rt2}" +
63
+ " {bc_}{user_s_class_attr} {ec_}" +
64
+ "{rt3}\n " +
65
+ "{bc_}{user_s_code}")
66
+ AP_EPILOG = """"""
67
+ INIT_PY = '__init__.py'
68
+ BASE_TEST_PY = 'base_test.py'
69
+ DO_NOT_REMOVE = [INIT_PY, BASE_TEST_PY]
70
+ TEST_EXT = '_test.py'
71
+
72
+ MODEL = None
73
+
74
+ def __get_full_class_name(schemaname, relationname):
75
+ schemaname = ''.join([elt.capitalize() for elt in schemaname.split('.')])
76
+ relationname = ''.join([elt.capitalize() for elt in relationname.split('_')])
77
+ return f'{schemaname}{relationname}'
78
+
79
+ def __get_field_desc(field_name, field):
80
+ #TODO: REFACTOR
81
+ sql_type = field._metadata['fieldtype']
82
+ field_desc = SQL_ADAPTER.get(sql_type)
83
+ if field_desc is None:
84
+ if not NO_APAPTER.get(sql_type):
85
+ NO_APAPTER[sql_type] = 0
86
+ NO_APAPTER[sql_type] += 1
87
+ field_desc = Any
88
+ if field_desc.__module__ != 'builtins':
89
+ HO_DATACLASSES_IMPORTS.add(field_desc.__module__)
90
+ ext = 'Any'
91
+ if hasattr(field_desc, '__name__'):
92
+ ext = field_desc.__name__
93
+ field_desc = f'{field_desc.__module__}.{ext}'
94
+ else:
95
+ field_desc = field_desc.__name__
96
+ value = 'dataclasses.field(default=None)'
97
+ if field._metadata['fieldtype'][0] == '_':
98
+ value = 'dataclasses.field(default_factory=list)'
99
+ field_desc = f'{field_desc} = {value}'
100
+ field_desc = f" {field_name}: {field_desc}"
101
+ error = utils.check_attribute_name(field_name)
102
+ if error:
103
+ field_desc = f'# {field_desc} FIX ME! {error}'
104
+ return field_desc
105
+
106
+ def __gen_dataclass(relation, fkeys):
107
+ rel = relation()
108
+ dc_name = relation._ho_dataclass_name()
109
+ fields = []
110
+ post_init = [' def __post_init__(self):']
111
+ for field_name, field in rel._ho_fields.items():
112
+ fields.append(__get_field_desc(field_name, field))
113
+ post_init.append(f' self.{field_name}: Field = None')
114
+
115
+ fkeys = {value:key for key, value in fkeys.items() if key != ''}
116
+ for key, value in rel()._ho_fkeys.items():
117
+ if key in fkeys:
118
+ fkey_alias = fkeys[key]
119
+ fdc_name = f'{value._FKey__relation._ho_dataclass_name()}'
120
+ post_init.append(f" self.{fkey_alias} = {fdc_name}")
121
+ return '\n'.join([f'@dataclasses.dataclass\nclass {dc_name}(DC_Relation):'] + fields + post_init)
122
+
123
+ def __get_modules_list(dir, files_list, files):
124
+ all_ = []
125
+ for file_ in files:
126
+ if re.findall(SKIP, file_):
127
+ continue
128
+ path_ = os.path.join(dir, file_)
129
+ if path_ not in files_list and file_ not in DO_NOT_REMOVE:
130
+ if path_.find('__pycache__') == -1 and path_.find(TEST_EXT) == -1:
131
+ print(f"REMOVING: {path_}")
132
+ os.remove(path_)
133
+ continue
134
+ if (re.findall('.py$', file_) and
135
+ file_ != INIT_PY and
136
+ file_ != '__pycache__' and
137
+ file_.find(TEST_EXT) == -1):
138
+ all_.append(file_.replace('.py', ''))
139
+ all_.sort()
140
+ return all_
141
+
142
+ def __update_init_files(package_dir, files_list, warning):
143
+ """Update __all__ lists in __init__ files.
144
+ """
145
+ for dir, _, files in os.walk(package_dir):
146
+ if dir == package_dir:
147
+ continue
148
+ reldir = dir.replace(package_dir, '')
149
+ if re.findall(SKIP, reldir):
150
+ continue
151
+ all_ = __get_modules_list(dir, files_list, files)
152
+ dirs = next(os.walk(dir))[1]
153
+
154
+ if len(all_) == 0 and dirs == ['__pycache__']:
155
+ shutil.rmtree(dir)
156
+ else:
157
+ with open(os.path.join(dir, INIT_PY), 'w', encoding='utf-8') as init_file:
158
+ init_file.write(f'"""{warning}"""\n\n')
159
+ all_ = ",\n ".join([f"'{elt}'" for elt in all_])
160
+ init_file.write(f'__all__ = [\n {all_}\n]\n')
161
+
162
+ def __get_inheritance_info(rel, package_name):
163
+ """Returns inheritance informations for the rel relation.
164
+ """
165
+ inheritance_import_list = []
166
+ inherited_classes_aliases_list = []
167
+ for base in rel.__class__.__bases__:
168
+ if base.__name__ != 'Relation':
169
+ inh_sfqrn = list(base._t_fqrn)
170
+ inh_sfqrn[0] = package_name
171
+ inh_cl_alias = f"{camel_case(inh_sfqrn[1])}{camel_case(inh_sfqrn[2])}"
172
+ inh_cl_name = f"{camel_case(inh_sfqrn[2])}"
173
+ from_import = f"from {'.'.join(inh_sfqrn)} import {inh_cl_name} as {inh_cl_alias}"
174
+ inheritance_import_list.append(from_import)
175
+ inherited_classes_aliases_list.append(inh_cl_alias)
176
+ inheritance_import = "\n".join(inheritance_import_list)
177
+ inherited_classes = ", ".join(inherited_classes_aliases_list)
178
+ if inherited_classes.strip():
179
+ inherited_classes = f"{inherited_classes}, "
180
+ return inheritance_import, inherited_classes
181
+
182
+ def __get_fkeys(repo, class_name, module_path):
183
+ try:
184
+ mod_path = module_path.replace(repo.base_dir, '').replace(os.path.sep, '.')[1:-3]
185
+ mod = importlib.import_module(mod_path)
186
+ importlib.reload(mod)
187
+ cls = mod.__dict__[class_name]
188
+ fkeys = cls.__dict__.get('Fkeys', {})
189
+ return fkeys
190
+ except ModuleNotFoundError:
191
+ pass
192
+ return {}
193
+
194
+ def __assemble_module_template(module_path):
195
+ """Construct the module after slicing it if it already exists.
196
+ """
197
+ ALT_BEGIN_CODE = "#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!\n"
198
+ user_s_code = ""
199
+ global_user_s_code = "\n"
200
+ module_template = MODULE_FORMAT
201
+ user_s_class_attr = ''
202
+ if os.path.exists(module_path):
203
+ module_code = utils.read(module_path)
204
+ if module_code.find(ALT_BEGIN_CODE) != -1:
205
+ module_code = module_code.replace(ALT_BEGIN_CODE, utils.BEGIN_CODE)
206
+ user_s_code = module_code.rsplit(utils.BEGIN_CODE, 1)[1]
207
+ user_s_code = user_s_code.replace('{', '{{').replace('}', '}}')
208
+ global_user_s_code = module_code.rsplit(utils.END_CODE)[0].split(utils.BEGIN_CODE)[1]
209
+ global_user_s_code = global_user_s_code.replace('{', '{{').replace('}', '}}')
210
+ user_s_class_attr = module_code.split(utils.BEGIN_CODE)[2].split(f' {utils.END_CODE}')[0]
211
+ user_s_class_attr = user_s_class_attr.replace('{', '{{').replace('}', '}}')
212
+ return module_template.format(
213
+ rt1=MODULE_TEMPLATE_1, rt2=MODULE_TEMPLATE_2, rt3=MODULE_TEMPLATE_3,
214
+ bc_=utils.BEGIN_CODE, ec_=utils.END_CODE,
215
+ global_user_s_code=global_user_s_code,
216
+ user_s_class_attr=user_s_class_attr,
217
+ user_s_code=user_s_code)
218
+
219
+ def __update_this_module(
220
+ repo, relation, package_dir, package_name):
221
+ """Updates the module."""
222
+ _, fqtn = relation
223
+ path = list(fqtn)
224
+ if path[1].find('half_orm_meta') == 0:
225
+ # hop internal. do nothing
226
+ return None
227
+ fqtn = '.'.join(path[1:])
228
+ try:
229
+ rel = repo.database.model.get_relation_class(fqtn)()
230
+ except (TypeError, UnknownRelation) as err:
231
+ sys.stderr.write(f"{err}\n{fqtn}\n")
232
+ sys.stderr.flush()
233
+ return None
234
+ fields = []
235
+ kwargs = []
236
+ arg_names = []
237
+ for key, value in rel._ho_fields.items():
238
+ error = utils.check_attribute_name(key)
239
+ if not error:
240
+ fields.append(f"self.{key}: Field = None")
241
+ kwarg_type = 'typing.Any'
242
+ if hasattr(value.py_type, '__name__'):
243
+ kwarg_type = str(value.py_type.__name__)
244
+ kwargs.append(f"{key}: '{kwarg_type}'=None")
245
+ arg_names.append(f'{key}={key}')
246
+ fields = "\n ".join(fields)
247
+ kwargs.append('**kwargs')
248
+ kwargs = ", ".join(kwargs)
249
+ arg_names = ", ".join(arg_names)
250
+ path[0] = package_dir
251
+ path[1] = path[1].replace('.', os.sep)
252
+
253
+ path = [iskeyword(elt) and f'{elt}_' or elt for elt in path]
254
+ class_name = camel_case(path[-1])
255
+ module_path = f"{os.path.join(*path)}.py"
256
+ path_1 = os.path.join(*path[:-1])
257
+ if not os.path.exists(path_1):
258
+ os.makedirs(path_1)
259
+ module_template = __assemble_module_template(module_path)
260
+ inheritance_import, inherited_classes = __get_inheritance_info(
261
+ rel, package_name)
262
+ with open(module_path, 'w', encoding='utf-8') as file_:
263
+ documentation = "\n".join([line and f" {line}" or "" for line in str(rel).split("\n")])
264
+ file_.write(
265
+ module_template.format(
266
+ hop_release = hop_version(),
267
+ module=f"{package_name}.{fqtn}",
268
+ package_name=package_name,
269
+ documentation=documentation,
270
+ inheritance_import=inheritance_import,
271
+ inherited_classes=inherited_classes,
272
+ class_name=class_name,
273
+ dc_name=rel._ho_dataclass_name(),
274
+ fqtn=fqtn,
275
+ kwargs=kwargs,
276
+ arg_names=arg_names,
277
+ warning=WARNING_TEMPLATE.format(package_name=package_name)))
278
+ if not os.path.exists(module_path.replace('.py', TEST_EXT)):
279
+ with open(module_path.replace('.py', TEST_EXT), 'w', encoding='utf-8') as file_:
280
+ file_.write(TEST.format(
281
+ BEGIN_CODE=utils.BEGIN_CODE,
282
+ END_CODE=utils.END_CODE,
283
+ package_name=package_name,
284
+ module=f"{package_name}.{fqtn}",
285
+ class_name=camel_case(path[-1]))
286
+ )
287
+ HO_DATACLASSES.append(__gen_dataclass(
288
+ rel, __get_fkeys(repo, class_name, module_path)))
289
+ return module_path
290
+
291
+ def __reset_dataclasses(repo, package_dir):
292
+ with open(os.path.join(package_dir, "ho_dataclasses.py"), "w", encoding='utf-8') as file_:
293
+ for relation in repo.database.model._relations():
294
+ t_qrn = relation[1][1:]
295
+ if t_qrn[0].find('half_orm') == 0:
296
+ continue
297
+ file_.write(f'class DC_{__get_full_class_name(*t_qrn)}: ...\n')
298
+
299
+ def __gen_dataclasses(package_dir, package_name):
300
+ with open(os.path.join(package_dir, "ho_dataclasses.py"), "w", encoding='utf-8') as file_:
301
+ file_.write(f"# dataclasses for {package_name}\n\n")
302
+ hd_imports = list(HO_DATACLASSES_IMPORTS)
303
+ hd_imports.sort()
304
+ for to_import in hd_imports:
305
+ file_.write(f"import {to_import}\n")
306
+ file_.write("\n")
307
+ for dc in HO_DATACLASSES:
308
+ file_.write(f"\n{dc}\n")
309
+
310
+ def generate(repo):
311
+ """Synchronize the modules with the structure of the relation in PG.
312
+ """
313
+ package_name = repo.name
314
+ package_dir = os.path.join(repo.base_dir, package_name)
315
+ files_list = []
316
+ try:
317
+ sql_adapter_module = importlib.import_module('.sql_adapter', package_name)
318
+ SQL_ADAPTER.update(sql_adapter_module.SQL_ADAPTER)
319
+ except ModuleNotFoundError as exc:
320
+ os.makedirs(package_dir)
321
+ with open(os.path.join(package_dir, 'sql_adapter.py'), "w") as file_:
322
+ file_.write(SQL_ADAPTER_TEMPLATE)
323
+ sys.stderr.write(f"{exc}\n")
324
+ except AttributeError as exc:
325
+ sys.stderr.write(f"{exc}\n")
326
+ repo.database.model._reload()
327
+ if not os.path.exists(package_dir):
328
+ os.mkdir(package_dir)
329
+
330
+ __reset_dataclasses(repo, package_dir)
331
+
332
+ with open(os.path.join(package_dir, INIT_PY), 'w', encoding='utf-8') as file_:
333
+ file_.write(INIT_MODULE_TEMPLATE.format(package_name=package_name))
334
+
335
+ if not os.path.exists(os.path.join(package_dir, BASE_TEST_PY)):
336
+ with open(os.path.join(package_dir, BASE_TEST_PY), 'w', encoding='utf-8') as file_:
337
+ file_.write(BASE_TEST.format(
338
+ BEGIN_CODE=utils.BEGIN_CODE,
339
+ END_CODE=utils.END_CODE,
340
+ package_name=package_name))
341
+ warning = WARNING_TEMPLATE.format(package_name=package_name)
342
+ for relation in repo.database.model._relations():
343
+ module_path = __update_this_module(repo, relation, package_dir, package_name)
344
+ if module_path:
345
+ files_list.append(module_path)
346
+ if module_path.find(INIT_PY) == -1:
347
+ test_file_path = module_path.replace('.py', TEST_EXT)
348
+ files_list.append(test_file_path)
349
+
350
+ __gen_dataclasses(package_dir, package_name)
351
+
352
+ if len(NO_APAPTER):
353
+ print("MISSING ADAPTER FOR SQL TYPE")
354
+ print(f"Add the following items to __SQL_ADAPTER in {os.path.join(package_dir, 'sql_adapter.py')}")
355
+ for key in NO_APAPTER.keys():
356
+ print(f" '{key}': typing.Any,")
357
+ __update_init_files(package_dir, files_list, warning)