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
File without changes
@@ -0,0 +1,117 @@
1
+ """The changelog module
2
+
3
+ Manages the CHANGELOG file. The file contains the log of the patches (released) and in preparation.
4
+
5
+ A line is of the form:
6
+ <hop version>\t<release number>\t<commit>\t<previous commit>
7
+
8
+ * hop version allows to check that the good hop version is used to apply the patch in production
9
+ * release number is an ordered list of release number
10
+ * commit is the git sha1 corresponding to the release of the patch. If empty, the patch is in
11
+ preparation.
12
+ * previous commit is the last commit on hop_main before the rebase of hop_<release>
13
+
14
+ """
15
+
16
+ import os
17
+
18
+ from half_orm import utils
19
+ from .utils import hop_version
20
+
21
+ class Changelog:
22
+ "The Changelog class..."
23
+ __log_list = []
24
+ __log_dict = {}
25
+ __releases = []
26
+ def __init__(self, repo):
27
+ self.__repo = repo
28
+ self.__file = os.path.join(self.__repo.base_dir, '.hop', 'CHANGELOG')
29
+ if not os.path.exists(self.__file):
30
+ utils.write(
31
+ self.__file,
32
+ f'{hop_version()}\t{self.__repo.database.last_release_s}\tInitial\t\n')
33
+ self.__repo.hgit.add(self.__file)
34
+ self.__repo.hgit.commit('-m', '[hop] Initial CHANGELOG')
35
+ self.__seq()
36
+
37
+ def __seq(self):
38
+ self.__log_list = [elt.strip('\n').split('\t') for elt in utils.readlines(self.__file)]
39
+ self.__log_dict = {elt[1]: elt for elt in self.__log_list}
40
+ self.__releases = list(self.__log_dict.keys())
41
+
42
+ @staticmethod
43
+ def _sort_releases(releases):
44
+ "Sort the releases"
45
+ int_releases = list([tuple(int(elt) for elt in release.split('.'))
46
+ for release in releases])
47
+ int_releases.sort()
48
+ releases = list([".".join(str(elt) for elt in release)
49
+ for release in int_releases])
50
+ return releases
51
+
52
+ def new_release(self, release):
53
+ """Update with the release the .hop/CHANGELOG file"""
54
+ releases = self.__releases
55
+ releases.append(release)
56
+ releases = Changelog._sort_releases(releases)
57
+ utils.write(self.__file, '')
58
+ for elt in releases:
59
+ rel = self.__log_dict.get(elt)
60
+ if rel:
61
+ utils.write(self.__file, f'{rel[0]}\t{rel[1]}\t{rel[2]}\t{rel[3]}\n', mode='a+')
62
+ else:
63
+ utils.write(self.__file, f'{hop_version()}\t{release}\t\t\n', mode='a+')
64
+ self.__seq()
65
+
66
+ def update_release(self, release, commit, previous_commit):
67
+ "Add the commit sha1 to the release in the .hop/CHANGELOG file"
68
+ out = []
69
+ previous = self.previous(release, 1)
70
+ for line in utils.readlines(self.__file):
71
+ if line and line.split()[1] not in {release, previous}:
72
+ out.append(line)
73
+ elif line.split()[1] == previous:
74
+ elt = line.split()
75
+ out.append(f'{elt[0]}\t{elt[1]}\t{elt[2]}\t{previous_commit}\n')
76
+ else:
77
+ out.append(f'{hop_version()}\t{release}\t{commit}\t\n')
78
+ utils.write(self.__file, ''.join(out))
79
+ self.__repo.hgit.add(self.__file)
80
+ # self.__repo.hgit.commit('-m', f'[hop][{release}] CHANGELOG')
81
+ self.__seq()
82
+
83
+ def previous(self, release, index):
84
+ "Return previous release of release."
85
+ index_of_release = self.__releases.index(release)
86
+ return self.__releases[index_of_release - index]
87
+
88
+ @property
89
+ def file(self):
90
+ "Return the name of the changelog file"
91
+ return self.__file
92
+
93
+ @property
94
+ def last_release(self):
95
+ "Return the sequence"
96
+ releases = [elt[1] for elt in self.__log_list if elt[2]]
97
+ return releases[-1]
98
+
99
+ @property
100
+ def releases_in_dev(self):
101
+ "Returns the list of patches in dev (not released)"
102
+ return [elt[1] for elt in self.__log_list if not elt[2]]
103
+
104
+ @property
105
+ def releases_to_apply_in_prod(self):
106
+ "Returns the list of releases to apply in production"
107
+ current = self.__repo.database.last_release_s
108
+ releases_to_apply = []
109
+ to_apply = False
110
+ for elt in self.__log_list:
111
+ if elt[1] == current:
112
+ # we're here
113
+ to_apply = True
114
+ continue
115
+ if to_apply and elt[2]:
116
+ releases_to_apply.append(elt[1])
117
+ return releases_to_apply
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ CLI extension integration for half-orm-dev
6
+
7
+ Provides the halfORM development tools through the unified half_orm CLI interface.
8
+ Generates/Patches/Synchronizes a hop Python package with a PostgreSQL database.
9
+ """
10
+
11
+ import sys
12
+ import click
13
+ from half_orm.cli_utils import create_and_register_extension
14
+
15
+ # Import existing halfORM_dev functionality
16
+ from half_orm_dev.repo import Repo
17
+ from half_orm import utils
18
+
19
+ class Hop:
20
+ """Sets the options available to the hop command"""
21
+ __available_cmds = []
22
+ __command = None
23
+
24
+ def __init__(self):
25
+ self.__repo: Repo = Repo()
26
+ if not self.repo_checked:
27
+ Hop.__available_cmds = ['new']
28
+ else:
29
+ if not self.__repo.devel:
30
+ # Sync-only mode
31
+ Hop.__available_cmds = ['sync-package']
32
+ else:
33
+ # Full mode - check environment
34
+ if self.__repo.production:
35
+ Hop.__available_cmds = ['upgrade', 'restore']
36
+ else:
37
+ Hop.__available_cmds = ['prepare', 'apply', 'release', 'undo']
38
+
39
+ @property
40
+ def repo_checked(self):
41
+ """Returns whether we are in a repo or not."""
42
+ return self.__repo.checked
43
+
44
+ @property
45
+ def model(self):
46
+ """Returns the model (half_orm.model.Model) associated to the repo."""
47
+ return self.__repo.model
48
+
49
+ @property
50
+ def state(self):
51
+ """Returns the state of the repo."""
52
+ return self.__repo.state
53
+
54
+ @property
55
+ def command(self):
56
+ """The command invoked (click)"""
57
+ return self.__command
58
+
59
+ def add_commands(main_group):
60
+ """
61
+ Required entry point for halfORM extensions.
62
+
63
+ Args:
64
+ main_group: The main Click group for the half_orm command
65
+ """
66
+
67
+ # Create hop instance to determine available commands
68
+ hop = Hop()
69
+
70
+ @create_and_register_extension(main_group, sys.modules[__name__])
71
+ def dev():
72
+ """halfORM development tools - project management, patches, and database synchronization"""
73
+ pass
74
+
75
+ # Define all possible commands
76
+ @click.command()
77
+ @click.argument('package_name')
78
+ @click.option('-d', '--devel', is_flag=True, help="Development mode")
79
+ def new(package_name, devel=False):
80
+ """Creates a new hop project named <package_name>."""
81
+ hop._Hop__repo.init(package_name, devel)
82
+
83
+ @click.command()
84
+ @click.option(
85
+ '-l', '--level',
86
+ type=click.Choice(['patch', 'minor', 'major']), help="Release level.")
87
+ @click.option('-m', '--message', type=str, help="The git commit message")
88
+ def prepare(level, message=None):
89
+ """Prepares the next release."""
90
+ hop._Hop__command = 'prepare'
91
+ hop._Hop__repo.prepare_release(level, message)
92
+ sys.exit()
93
+
94
+ @click.command()
95
+ def apply():
96
+ """Apply the current release."""
97
+ hop._Hop__command = 'apply'
98
+ hop._Hop__repo.apply_release()
99
+
100
+ @click.command()
101
+ @click.option(
102
+ '-d', '--database-only', is_flag=True,
103
+ help='Restore the database to the previous release.')
104
+ def undo(database_only):
105
+ """Undo the last release."""
106
+ hop._Hop__command = 'undo'
107
+ hop._Hop__repo.undo_release(database_only)
108
+
109
+ @click.command()
110
+ def upgrade():
111
+ """Apply one or many patches.
112
+
113
+ Switches to hop_main, pulls should check the tags.
114
+ """
115
+ hop._Hop__command = 'upgrade_prod'
116
+ hop._Hop__repo.upgrade_prod()
117
+
118
+ @click.command()
119
+ @click.argument('release')
120
+ def restore(release):
121
+ """Restore to release."""
122
+ hop._Hop__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
+ """Commit and optionally push the current release."""
128
+ hop._Hop__repo.commit_release(push)
129
+
130
+ @click.command()
131
+ def sync_package():
132
+ """Synchronize the Python package with the database model."""
133
+ hop._Hop__repo.sync_package()
134
+
135
+ # Map command names to command functions
136
+ all_commands = {
137
+ 'new': new,
138
+ 'prepare': prepare,
139
+ 'apply': apply,
140
+ 'undo': undo,
141
+ 'release': release,
142
+ 'sync-package': sync_package,
143
+ 'upgrade': upgrade,
144
+ 'restore': restore
145
+ }
146
+
147
+ # 🎯 COMPORTEMENT ADAPTATIF RESTAURÉ
148
+ # Only add commands that are available in the current context
149
+ for cmd_name in hop._Hop__available_cmds:
150
+ if cmd_name in all_commands:
151
+ dev.add_command(all_commands[cmd_name])
152
+
153
+ # Add callback to show state when no subcommand (like original hop)
154
+ original_callback = dev.callback
155
+
156
+ @click.pass_context
157
+ def enhanced_callback(ctx, *args, **kwargs):
158
+ if ctx.invoked_subcommand is None:
159
+ # Show repo state when no subcommand is provided
160
+ if hop.repo_checked:
161
+ click.echo(hop.state)
162
+ else:
163
+ click.echo(hop.state)
164
+ click.echo("\nNot in a hop repository.")
165
+ click.echo(f"Try {utils.Color.bold('half_orm dev new [--devel] <package_name>')} or change directory.\n")
166
+ else:
167
+ # Call original callback if there is one
168
+ if original_callback:
169
+ return original_callback(*args, **kwargs)
170
+
171
+ dev.callback = enhanced_callback
@@ -0,0 +1,127 @@
1
+ """Provides the Database class
2
+ """
3
+
4
+ import os
5
+ import subprocess
6
+ import sys
7
+
8
+ from psycopg2 import OperationalError
9
+ from half_orm.model import Model
10
+ from half_orm.model_errors import UnknownRelation
11
+ from half_orm import utils
12
+ from half_orm_dev.db_conn import DbConn
13
+ from .utils import HOP_PATH
14
+
15
+
16
+ class Database:
17
+ """Reads and writes the halfORM connection file
18
+ """
19
+
20
+ def __init__(self, repo, get_release=True):
21
+ self.__repo = repo
22
+ self.__model = None
23
+ self.__last_release = None
24
+ self.__connection_params: DbConn = DbConn(self.__repo.name)
25
+ if self.__repo.name:
26
+ try:
27
+ self.__model = Model(self.__repo.name)
28
+ self.__init(self.__repo.name, get_release)
29
+ except OperationalError as err:
30
+ if not self.__repo.new:
31
+ utils.error(err, 1)
32
+
33
+ def __call__(self, name):
34
+ return self.__class__(self.__repo)
35
+
36
+ def __init(self, name, get_release=True):
37
+ self.__name = name
38
+ self.__connection_params = DbConn(name)
39
+ if get_release and self.__repo.devel:
40
+ self.__last_release = self.last_release
41
+
42
+ @property
43
+ def last_release(self):
44
+ "Returns the last release"
45
+ self.__last_release = next(
46
+ self.__model.get_relation_class('half_orm_meta.view.hop_last_release')().ho_select())
47
+ return self.__last_release
48
+
49
+ @property
50
+ def last_release_s(self):
51
+ "Returns the string representation of the last release X.Y.Z"
52
+ return '{major}.{minor}.{patch}'.format(**self.last_release)
53
+
54
+ @property
55
+ def model(self):
56
+ "The model (halfORM) of the database"
57
+ return self.__model
58
+
59
+ @property
60
+ def state(self):
61
+ "The state (str) of the database"
62
+ res = ['[Database]']
63
+ res.append(f'- name: {self.__name}')
64
+ res.append(f'- user: {self.__connection_params.user}')
65
+ res.append(f'- host: {self.__connection_params.host}')
66
+ res.append(f'- port: {self.__connection_params.port}')
67
+ prod = utils.Color.blue(
68
+ True) if self.__connection_params.production else False
69
+ res.append(f'- production: {prod}')
70
+ if self.__repo.devel:
71
+ res.append(f'- last release: {self.last_release_s}')
72
+ return '\n'.join(res)
73
+
74
+ @property
75
+ def production(self):
76
+ "Returns wether the database is tagged in production or not."
77
+ return self.__connection_params.production
78
+
79
+ def init(self, name):
80
+ """Called when creating a new repo.
81
+ Tries to read the connection parameters and then connect to
82
+ the database.
83
+ """
84
+ try:
85
+ self.__init(name, get_release=False)
86
+ except FileNotFoundError:
87
+ pass
88
+ return self.__init_db()
89
+
90
+ def __init_db(self):
91
+ """Tries to connect to the database. If unsuccessful, creates the
92
+ database end initializes it with half_orm_meta.
93
+ """
94
+ try:
95
+ self.__model = Model(self.__name)
96
+ except OperationalError:
97
+ sys.stderr.write(f"The database '{self.__name}' does not exist.\n")
98
+ create = input('Do you want to create it (Y/n): ') or "y"
99
+ if create.upper() == 'Y':
100
+ self.execute_pg_command('createdb')
101
+ else:
102
+ utils.error(
103
+ f'Aborting! Please remove {self.__name} directory.\n', exit_code=1)
104
+ self.__model = Model(self.__name)
105
+ if self.__repo.devel:
106
+ try:
107
+ self.__model.get_relation_class('half_orm_meta.hop_release')
108
+ except UnknownRelation:
109
+ hop_init_sql_file = os.path.join(
110
+ HOP_PATH, 'patches', 'sql', 'half_orm_meta.sql')
111
+ self.execute_pg_command(
112
+ 'psql', '-f', hop_init_sql_file, stdout=subprocess.DEVNULL)
113
+ self.__model.reconnect(reload=True)
114
+ self.__last_release = self.register_release(
115
+ major=0, minor=0, patch=0, changelog='Initial release')
116
+ return self(self.__name)
117
+
118
+ @property
119
+ def execute_pg_command(self):
120
+ "Helper: execute a postgresql command"
121
+ return self.__connection_params.execute_pg_command
122
+
123
+ def register_release(self, major, minor, patch, changelog):
124
+ "Register the release into half_orm_meta.hop_release"
125
+ return self.__model.get_relation_class('half_orm_meta.hop_release')(
126
+ major=major, minor=minor, patch=patch, changelog=changelog
127
+ ).ho_insert()
@@ -0,0 +1,134 @@
1
+ """Provides the DbConn class.
2
+ """
3
+
4
+ import os
5
+ import subprocess
6
+ import sys
7
+
8
+ from getpass import getpass
9
+ from configparser import ConfigParser
10
+
11
+ from half_orm.model import CONF_DIR
12
+ from half_orm import utils
13
+
14
+ CONF_NOT_FOUND = '''
15
+ The configuration file {} is missing.
16
+ You must create it before proceeding.
17
+
18
+ '''
19
+
20
+ class DbConn:
21
+ """Handles the connection parameters to the database.
22
+ Provides the execute_pg_command."""
23
+ __conf_dir = CONF_DIR # HALFORM_CONF_DIR
24
+ def __init__(self, name):
25
+ self.__name = name
26
+ self.__user = None
27
+ self.__password = None
28
+ self.__host = None
29
+ self.__port = None
30
+ self.__production = None
31
+ if name:
32
+ self.__connection_file = os.path.join(self.__conf_dir, self.__name)
33
+ if os.path.exists(self.__connection_file):
34
+ self.__init()
35
+
36
+ @property
37
+ def production(self):
38
+ "prod"
39
+ return self.__production
40
+
41
+ def __init(self):
42
+ "Reads the config file and sets the connection parameters"
43
+ config = ConfigParser()
44
+ config.read([self.__connection_file])
45
+
46
+ self.__name = config.get('database', 'name')
47
+
48
+ self.__user = config.get('database', 'user', fallback=None)
49
+ self.__password = config.get('database', 'password', fallback=None)
50
+ self.__host = config.get('database', 'host', fallback=None)
51
+ self.__port = config.get('database', 'port', fallback=None)
52
+
53
+ self.__production = config.getboolean('database', 'production', fallback=False)
54
+
55
+ @property
56
+ def host(self):
57
+ "Returns host name"
58
+ return self.__host
59
+ @property
60
+ def port(self):
61
+ "Returns port"
62
+ return self.__port
63
+ @property
64
+ def user(self):
65
+ "Returns user"
66
+ return self.__user
67
+
68
+ def set_params(self, name):
69
+ """Asks for the connection parameters.
70
+ """
71
+ if not os.access(self.__conf_dir, os.W_OK):
72
+ sys.stderr.write(f"You don't have write access to {self.__conf_dir}.\n")
73
+ if self.__conf_dir == '/etc/half_orm': # only on linux
74
+ utils.error(
75
+ "Set the HALFORM_CONF_DIR environment variable if you want to use a\n"
76
+ "different directory.\n", exit_code=1)
77
+ print(f'Connection parameters to the database {self.__name}:')
78
+ self.__user = os.environ['USER']
79
+ self.__user = input(f'. user ({self.__user}): ') or self.__user
80
+ self.__password = getpass('. password: ')
81
+ if self.__password == '' and \
82
+ (input(
83
+ '. is it an ident login with a local account? [Y/n] ') or 'Y').upper() == 'Y':
84
+ self.__host = self.__port = ''
85
+ else:
86
+ self.__host = input('. host (localhost): ') or 'localhost'
87
+ self.__port = input('. port (5432): ') or 5432
88
+
89
+ self.__production = input('Production (False): ') or False
90
+
91
+ self.__write_config()
92
+
93
+ return self
94
+
95
+ def __write_config(self):
96
+ "Helper: write file in utf8"
97
+ self.__connection_file = os.path.join(self.__conf_dir, self.__name)
98
+ config = ConfigParser()
99
+ config['database'] = {
100
+ 'name': self.__name,
101
+ 'user': self.__user,
102
+ 'password': self.__password,
103
+ 'host': self.__host,
104
+ 'port': self.__port,
105
+ 'production': self.__production
106
+ }
107
+ with open(self.__connection_file, 'w', encoding='utf-8') as configfile:
108
+ config.write(configfile)
109
+
110
+ def execute_pg_command(self, cmd, *args, **kwargs):
111
+ """Helper. Executes a postgresql
112
+ """
113
+ if not kwargs.get('stdout'):
114
+ kwargs['stdout'] = subprocess.DEVNULL
115
+ cmd_list = [cmd]
116
+ env = os.environ.copy()
117
+ password = self.__password
118
+ if password:
119
+ env['PGPASSWORD'] = password
120
+ if self.__user:
121
+ cmd_list += ['-U', self.__user]
122
+ if self.__port:
123
+ cmd_list += ['-p', self.__port]
124
+ if self.__host:
125
+ cmd_list += ['-h', self.__host]
126
+ cmd_list.append(self.__name)
127
+ if args:
128
+ cmd_list += args
129
+ try:
130
+ subprocess.run(
131
+ cmd_list, env=env, shell=False, check=True,
132
+ **kwargs)
133
+ except subprocess.CalledProcessError as err:
134
+ utils.error(f'{err}\ndatabase: {self.__name} with user: {self.__user}, host: {self.__host}, port: {self.__port}\n', exit_code=err.returncode)