hypershell 2.5.1__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.
- hypershell/__init__.py +114 -0
- hypershell/client.py +1256 -0
- hypershell/cluster/__init__.py +378 -0
- hypershell/cluster/local.py +209 -0
- hypershell/cluster/remote.py +720 -0
- hypershell/cluster/ssh.py +351 -0
- hypershell/config.py +452 -0
- hypershell/core/__init__.py +4 -0
- hypershell/core/config.py +357 -0
- hypershell/core/exceptions.py +120 -0
- hypershell/core/fsm.py +67 -0
- hypershell/core/heartbeat.py +70 -0
- hypershell/core/logging.py +201 -0
- hypershell/core/platform.py +121 -0
- hypershell/core/queue.py +152 -0
- hypershell/core/remote.py +192 -0
- hypershell/core/signal.py +57 -0
- hypershell/core/template.py +201 -0
- hypershell/core/thread.py +56 -0
- hypershell/core/types.py +32 -0
- hypershell/data/__init__.py +137 -0
- hypershell/data/core.py +225 -0
- hypershell/data/model.py +630 -0
- hypershell/server.py +1210 -0
- hypershell/submit.py +806 -0
- hypershell/task.py +1158 -0
- hypershell-2.5.1.dist-info/LICENSE +201 -0
- hypershell-2.5.1.dist-info/METADATA +114 -0
- hypershell-2.5.1.dist-info/RECORD +31 -0
- hypershell-2.5.1.dist-info/WHEEL +4 -0
- hypershell-2.5.1.dist-info/entry_points.txt +4 -0
hypershell/data/core.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Geoffrey Lentner
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Core interface for database engine and session manager."""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# type annotations
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from typing import Any, Type
|
|
10
|
+
|
|
11
|
+
# standard libs
|
|
12
|
+
import sys
|
|
13
|
+
import logging
|
|
14
|
+
from urllib.parse import urlencode
|
|
15
|
+
|
|
16
|
+
# external libs
|
|
17
|
+
from cmdkit.app import exit_status
|
|
18
|
+
from cmdkit.config import Namespace, ConfigurationError
|
|
19
|
+
from sqlalchemy.engine import create_engine, Engine
|
|
20
|
+
from sqlalchemy.pool import StaticPool
|
|
21
|
+
from sqlalchemy.orm import sessionmaker, scoped_session
|
|
22
|
+
from sqlalchemy.exc import ArgumentError
|
|
23
|
+
|
|
24
|
+
# internal libs
|
|
25
|
+
from hypershell.core.config import config
|
|
26
|
+
from hypershell.core.logging import handler
|
|
27
|
+
from hypershell.core.exceptions import write_traceback
|
|
28
|
+
|
|
29
|
+
# public interface
|
|
30
|
+
__all__ = ['DatabaseURL', 'engine', 'Session', 'config', 'in_memory', 'schema', ]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DatabaseURL(dict):
|
|
34
|
+
"""
|
|
35
|
+
Dataclass-like representation for database URL.
|
|
36
|
+
Standard arguments apply. Extra arguments are encoded as URL parameters.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> url = DatabaseURL(provider='postgresql', database='mine')
|
|
40
|
+
>>> url.encode()
|
|
41
|
+
'postgresql://localhost/mine'
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self: DatabaseURL, **fields) -> None:
|
|
45
|
+
"""Initialize fields."""
|
|
46
|
+
try:
|
|
47
|
+
super().__init__(provider=fields.pop('provider'),
|
|
48
|
+
database=fields.pop('database', None),
|
|
49
|
+
file=fields.pop('file', None),
|
|
50
|
+
user=fields.pop('user', None),
|
|
51
|
+
password=fields.pop('password', None),
|
|
52
|
+
host=fields.pop('host', None),
|
|
53
|
+
port=fields.pop('port', None),
|
|
54
|
+
parameters=fields)
|
|
55
|
+
except KeyError as _error:
|
|
56
|
+
raise AttributeError('Missing \'provider\'') from _error
|
|
57
|
+
self._validate()
|
|
58
|
+
|
|
59
|
+
def __getattr__(self: DatabaseURL, field: str) -> Any:
|
|
60
|
+
return self.get(field)
|
|
61
|
+
|
|
62
|
+
def __repr__(self: DatabaseURL) -> str:
|
|
63
|
+
"""Interactive representation."""
|
|
64
|
+
masked = self.__class__(**self)
|
|
65
|
+
masked['password'] = None if self.password is None else '****'
|
|
66
|
+
value = '<DatabaseURL('
|
|
67
|
+
value += ', '.join([field + '=' + repr(masked.get(field))
|
|
68
|
+
for field in ('provider', 'database', 'file', 'user', 'password', 'host', 'port')
|
|
69
|
+
if masked.get(field) is not None])
|
|
70
|
+
if self.parameters:
|
|
71
|
+
value += ', ' + ', '.join([field + '=' + repr(value)
|
|
72
|
+
for field, value in self.parameters.items()])
|
|
73
|
+
return value + ')>'
|
|
74
|
+
|
|
75
|
+
def _validate(self: DatabaseURL) -> None:
|
|
76
|
+
"""Validate provided arguments."""
|
|
77
|
+
if self.provider == 'sqlite':
|
|
78
|
+
self._validate_for_sqlite()
|
|
79
|
+
else:
|
|
80
|
+
self._validate_database()
|
|
81
|
+
self._validate_user_and_password()
|
|
82
|
+
|
|
83
|
+
def _validate_user_and_password(self: DatabaseURL) -> None:
|
|
84
|
+
if self.user is not None and self.password is None:
|
|
85
|
+
raise AttributeError('Must provide \'password\' if \'user\' provided')
|
|
86
|
+
if self.user is None and self.password is not None:
|
|
87
|
+
raise AttributeError('Must provide \'user\' if \'password\' provided')
|
|
88
|
+
|
|
89
|
+
def _validate_for_sqlite(self: DatabaseURL) -> None:
|
|
90
|
+
if self.file is not None and self.database is not None:
|
|
91
|
+
raise AttributeError('Cannot provide both \'file\' and \'database\' for SQLite')
|
|
92
|
+
for field in ('user', 'password', 'host', 'port'):
|
|
93
|
+
if self.get(field) is not None:
|
|
94
|
+
raise AttributeError(f'Cannot provide \'{field}\' for SQLite')
|
|
95
|
+
|
|
96
|
+
def _validate_database(self: DatabaseURL) -> None:
|
|
97
|
+
if self.file:
|
|
98
|
+
raise AttributeError('Cannot provide \'file\' if not SQLite')
|
|
99
|
+
if not self.database:
|
|
100
|
+
raise AttributeError('Must provide \'database\' if not SQLite')
|
|
101
|
+
|
|
102
|
+
def encode(self: DatabaseURL) -> str:
|
|
103
|
+
"""Construct URL string with encoded parameters."""
|
|
104
|
+
return ''.join([
|
|
105
|
+
f'{self.provider}://',
|
|
106
|
+
self._format_user_and_password(),
|
|
107
|
+
self._format_host_and_port(),
|
|
108
|
+
self._format_database_or_file(),
|
|
109
|
+
self._format_parameters(),
|
|
110
|
+
])
|
|
111
|
+
|
|
112
|
+
def _format_parameters(self: DatabaseURL) -> str:
|
|
113
|
+
if self.parameters:
|
|
114
|
+
return '?' + urlencode(self.parameters)
|
|
115
|
+
else:
|
|
116
|
+
return ''
|
|
117
|
+
|
|
118
|
+
def _format_database_or_file(self: DatabaseURL) -> str:
|
|
119
|
+
if self.database:
|
|
120
|
+
return f'/{self.database}'
|
|
121
|
+
elif self.file:
|
|
122
|
+
return f'/{self.file}'
|
|
123
|
+
else:
|
|
124
|
+
return ''
|
|
125
|
+
|
|
126
|
+
def _format_host_and_port(self: DatabaseURL) -> str:
|
|
127
|
+
if self.host and self.port:
|
|
128
|
+
return f'{self.host}:{self.port}'
|
|
129
|
+
elif self.host and not self.port:
|
|
130
|
+
return f'{self.host}'
|
|
131
|
+
elif self.port and not self.host:
|
|
132
|
+
return f'localhost:{self.port}'
|
|
133
|
+
else:
|
|
134
|
+
if self.user or self.password:
|
|
135
|
+
return 'localhost'
|
|
136
|
+
else:
|
|
137
|
+
return ''
|
|
138
|
+
|
|
139
|
+
def _format_user_and_password(self: DatabaseURL) -> str:
|
|
140
|
+
if self.user and self.password:
|
|
141
|
+
return f'{self.user}:{self.password}@'
|
|
142
|
+
else:
|
|
143
|
+
return ''
|
|
144
|
+
|
|
145
|
+
def __str__(self: DatabaseURL) -> str:
|
|
146
|
+
return self.encode()
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def _strip_endings(value: str, *endings: str):
|
|
150
|
+
"""Removing instances of each possible `endings` from `value`."""
|
|
151
|
+
r = str(value)
|
|
152
|
+
for ending in endings:
|
|
153
|
+
pos = len(ending)
|
|
154
|
+
if r[-pos:] == ending:
|
|
155
|
+
r = r[:-pos]
|
|
156
|
+
return r
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def from_namespace(cls: Type[DatabaseURL], ns: Namespace) -> DatabaseURL:
|
|
160
|
+
fields = {}
|
|
161
|
+
for key in ns.keys():
|
|
162
|
+
key_ = cls._strip_endings(key, '_env', '_eval')
|
|
163
|
+
fields[key_] = getattr(ns, key_)
|
|
164
|
+
return cls(**fields)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# Allowed database providers
|
|
168
|
+
# Mapping translates from name to library/implementation name
|
|
169
|
+
# NOTE: mysql/mariadb and other providers not yet working
|
|
170
|
+
providers = {
|
|
171
|
+
'sqlite': 'sqlite',
|
|
172
|
+
'postgres': 'postgresql',
|
|
173
|
+
'postgresql': 'postgresql',
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
config = Namespace(config.database.copy())
|
|
178
|
+
schema = config.pop('schema', None)
|
|
179
|
+
engine_echo = config.pop('echo', False)
|
|
180
|
+
connect_args = config.pop('connect_args', {})
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# additional parameters for engine creation
|
|
184
|
+
engine_config = {}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# sqlite specific configuration
|
|
188
|
+
in_memory = False
|
|
189
|
+
if config.provider == 'sqlite':
|
|
190
|
+
in_memory = (config.get('file', None) or config.get('database', None)) in ('', ':memory:', None)
|
|
191
|
+
if in_memory:
|
|
192
|
+
engine_config['poolclass'] = StaticPool
|
|
193
|
+
if 'check_same_thread' not in connect_args:
|
|
194
|
+
connect_args['check_same_thread'] = False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def get_url() -> DatabaseURL:
|
|
198
|
+
"""Wraps parsing within function."""
|
|
199
|
+
if config.provider not in providers:
|
|
200
|
+
raise ConfigurationError(f'Unsupported database \'{config.provider}\'')
|
|
201
|
+
try:
|
|
202
|
+
params = Namespace({**config, 'provider': providers[config.provider]})
|
|
203
|
+
return DatabaseURL.from_namespace(params)
|
|
204
|
+
except AttributeError as err:
|
|
205
|
+
raise ConfigurationError(str(err)) from err
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def get_engine() -> Engine:
|
|
209
|
+
"""Wraps engine creation."""
|
|
210
|
+
try:
|
|
211
|
+
if engine_echo:
|
|
212
|
+
logging.getLogger('sqlalchemy.engine').addHandler(handler)
|
|
213
|
+
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
|
|
214
|
+
return create_engine(get_url().encode(), connect_args=connect_args, **engine_config)
|
|
215
|
+
except ArgumentError as err:
|
|
216
|
+
raise ConfigurationError(f'DatabaseURL: {err}') from err
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
engine = get_engine()
|
|
221
|
+
factory = sessionmaker(bind=engine)
|
|
222
|
+
Session = scoped_session(factory)
|
|
223
|
+
except Exception as error:
|
|
224
|
+
write_traceback(error, module=__name__)
|
|
225
|
+
sys.exit(exit_status.bad_config)
|