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.
- half_orm_dev/__init__.py +1 -0
- half_orm_dev/cli/__init__.py +9 -0
- half_orm_dev/cli/commands/__init__.py +56 -0
- half_orm_dev/cli/commands/apply.py +13 -0
- half_orm_dev/cli/commands/clone.py +102 -0
- half_orm_dev/cli/commands/init.py +331 -0
- half_orm_dev/cli/commands/new.py +15 -0
- half_orm_dev/cli/commands/patch.py +317 -0
- half_orm_dev/cli/commands/prepare.py +21 -0
- half_orm_dev/cli/commands/prepare_release.py +119 -0
- half_orm_dev/cli/commands/promote_to.py +127 -0
- half_orm_dev/cli/commands/release.py +344 -0
- half_orm_dev/cli/commands/restore.py +14 -0
- half_orm_dev/cli/commands/sync.py +13 -0
- half_orm_dev/cli/commands/todo.py +73 -0
- half_orm_dev/cli/commands/undo.py +17 -0
- half_orm_dev/cli/commands/update.py +73 -0
- half_orm_dev/cli/commands/upgrade.py +191 -0
- half_orm_dev/cli/main.py +103 -0
- half_orm_dev/cli_extension.py +38 -0
- half_orm_dev/database.py +1389 -0
- half_orm_dev/hgit.py +1025 -0
- half_orm_dev/hop.py +167 -0
- half_orm_dev/manifest.py +43 -0
- half_orm_dev/modules.py +456 -0
- half_orm_dev/patch.py +281 -0
- half_orm_dev/patch_manager.py +1694 -0
- half_orm_dev/patch_validator.py +335 -0
- half_orm_dev/patches/0/1/0/00_half_orm_meta.database.sql +34 -0
- half_orm_dev/patches/0/1/0/01_alter_half_orm_meta.hop_release.sql +2 -0
- half_orm_dev/patches/0/1/0/02_half_orm_meta.view.hop_penultimate_release.sql +3 -0
- half_orm_dev/patches/log +2 -0
- half_orm_dev/patches/sql/half_orm_meta.sql +208 -0
- half_orm_dev/release_manager.py +2841 -0
- half_orm_dev/repo.py +1562 -0
- half_orm_dev/templates/.gitignore +15 -0
- half_orm_dev/templates/MANIFEST.in +1 -0
- half_orm_dev/templates/Pipfile +13 -0
- half_orm_dev/templates/README +25 -0
- half_orm_dev/templates/conftest_template +42 -0
- half_orm_dev/templates/init_module_template +10 -0
- half_orm_dev/templates/module_template_1 +12 -0
- half_orm_dev/templates/module_template_2 +6 -0
- half_orm_dev/templates/module_template_3 +3 -0
- half_orm_dev/templates/relation_test +23 -0
- half_orm_dev/templates/setup.py +81 -0
- half_orm_dev/templates/sql_adapter +9 -0
- half_orm_dev/templates/warning +12 -0
- half_orm_dev/utils.py +49 -0
- half_orm_dev/version.txt +1 -0
- half_orm_dev-0.16.0a9.dist-info/METADATA +935 -0
- half_orm_dev-0.16.0a9.dist-info/RECORD +58 -0
- half_orm_dev-0.16.0a9.dist-info/WHEEL +5 -0
- half_orm_dev-0.16.0a9.dist-info/licenses/AUTHORS +3 -0
- half_orm_dev-0.16.0a9.dist-info/licenses/LICENSE +14 -0
- half_orm_dev-0.16.0a9.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- 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({})
|
half_orm_dev/manifest.py
ADDED
|
@@ -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
|
+
}))
|
half_orm_dev/modules.py
ADDED
|
@@ -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)
|