workercommon 0.5.2__tar.gz → 0.5.3__tar.gz
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.
- {workercommon-0.5.2 → workercommon-0.5.3}/PKG-INFO +1 -1
- {workercommon-0.5.2 → workercommon-0.5.3}/pyproject.toml +2 -1
- workercommon-0.5.3/workercommon/config/__init__.py +18 -0
- workercommon-0.5.3/workercommon/config/_configparser.py +81 -0
- workercommon-0.5.3/workercommon/config/_yaml.py +212 -0
- workercommon-0.5.3/workercommon/py.typed +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon.egg-info/PKG-INFO +1 -1
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon.egg-info/SOURCES.txt +4 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/LICENSE.TXT +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/README.md +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/setup.cfg +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/__init__.py +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/background.py +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/commands.py +0 -0
- {workercommon-0.5.2/workercommon/database → workercommon-0.5.3/workercommon/config}/py.typed +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/connectionpool.py +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/database/__init__.py +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/database/base.py +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/database/pg.py +0 -0
- {workercommon-0.5.2/workercommon → workercommon-0.5.3/workercommon/database}/py.typed +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/locking.py +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/rabbitmqueue.py +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/test.py +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/worker.py +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon.egg-info/dependency_links.txt +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon.egg-info/not-zip-safe +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon.egg-info/requires.txt +0 -0
- {workercommon-0.5.2 → workercommon-0.5.3}/workercommon.egg-info/top_level.txt +0 -0
|
@@ -18,7 +18,7 @@ dependencies = [
|
|
|
18
18
|
"psycopg2",
|
|
19
19
|
"pyyaml",
|
|
20
20
|
]
|
|
21
|
-
version = "0.5.
|
|
21
|
+
version = "0.5.3"
|
|
22
22
|
license-files = ["LICENSE.TXT"]
|
|
23
23
|
classifiers = [
|
|
24
24
|
"Programming Language :: Python :: 3",
|
|
@@ -35,6 +35,7 @@ Issues = "https://gitgud.io/takeshitakenji/workercommon/issues"
|
|
|
35
35
|
zip-safe = false
|
|
36
36
|
packages = [
|
|
37
37
|
"workercommon",
|
|
38
|
+
"workercommon.config",
|
|
38
39
|
"workercommon.database",
|
|
39
40
|
]
|
|
40
41
|
include-package-data = true
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
if sys.version_info < (3, 8):
|
|
5
|
+
raise RuntimeError("At least Python 3.8 is required")
|
|
6
|
+
|
|
7
|
+
from ._configparser import Configuration # noqa
|
|
8
|
+
try:
|
|
9
|
+
from ._yaml import ( # noqa
|
|
10
|
+
ConfigBase,
|
|
11
|
+
LogLevel,
|
|
12
|
+
Logging,
|
|
13
|
+
isoformat,
|
|
14
|
+
to_json,
|
|
15
|
+
to_yaml,
|
|
16
|
+
)
|
|
17
|
+
except ImportError:
|
|
18
|
+
pass
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
if sys.version_info < (3, 8):
|
|
5
|
+
raise RuntimeError("At least Python 3.8 is required")
|
|
6
|
+
|
|
7
|
+
from configparser import ConfigParser
|
|
8
|
+
import logging
|
|
9
|
+
from logging.handlers import WatchedFileHandler
|
|
10
|
+
from typing import Tuple, Optional, Callable
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Configuration(object):
|
|
15
|
+
def __init__(self, filename: str):
|
|
16
|
+
self.parser = self.create_parser()
|
|
17
|
+
self.set_base_config(self.parser)
|
|
18
|
+
self.parser.read(filename)
|
|
19
|
+
self.check_all_values(self.parser)
|
|
20
|
+
|
|
21
|
+
def create_parser(self) -> ConfigParser:
|
|
22
|
+
return ConfigParser()
|
|
23
|
+
|
|
24
|
+
def set_base_config(self, parser: ConfigParser) -> None:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def check_all_values(self, parser: ConfigParser) -> None:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def dir_exists(s: str) -> bool:
|
|
32
|
+
return bool(s and Path(s).is_dir())
|
|
33
|
+
|
|
34
|
+
def check_value(
|
|
35
|
+
self, section: str, key: str, validator: Callable[[str], bool]
|
|
36
|
+
) -> None:
|
|
37
|
+
value: Optional[str] = self[section, key]
|
|
38
|
+
try:
|
|
39
|
+
if value is None or not validator(value):
|
|
40
|
+
raise ValueError
|
|
41
|
+
except Exception as e:
|
|
42
|
+
raise ValueError(f"Invalid {section}/{key} value: {value}") from e
|
|
43
|
+
|
|
44
|
+
def __getitem__(self, parts: Tuple[str, str]) -> Optional[str]:
|
|
45
|
+
section, key = parts
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
value = self.parser[section][key]
|
|
49
|
+
return value if value else None
|
|
50
|
+
except KeyError:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
def get_strict(self, section: str, key: str) -> str:
|
|
54
|
+
value = self[section, key]
|
|
55
|
+
if value is None:
|
|
56
|
+
raise ValueError(f"Missing {section}/{key} value")
|
|
57
|
+
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
def get(self, section: str, key: str, default: str) -> str:
|
|
61
|
+
value = self[section, key]
|
|
62
|
+
return value if value is not None else default
|
|
63
|
+
|
|
64
|
+
def configure_logging(self, *handlers) -> None:
|
|
65
|
+
level = self["Logging", "level"]
|
|
66
|
+
if not level:
|
|
67
|
+
raise RuntimeError("Logging level should have already been validated")
|
|
68
|
+
|
|
69
|
+
log_format = self["Logging", "format"]
|
|
70
|
+
if not log_format:
|
|
71
|
+
log_format = "[%(asctime)s] [%(levelname)s] [%(module)s] [%(filename)s:%(lineno)d %(funcName)s] %(message)s" # noqa
|
|
72
|
+
|
|
73
|
+
if handlers:
|
|
74
|
+
logging.basicConfig(level=level, format=log_format, handlers=handlers)
|
|
75
|
+
else:
|
|
76
|
+
fname = self["Logging", "file"]
|
|
77
|
+
if fname:
|
|
78
|
+
handler = WatchedFileHandler(fname)
|
|
79
|
+
logging.basicConfig(level=level, format=log_format, handlers=[handler])
|
|
80
|
+
else:
|
|
81
|
+
logging.basicConfig(level=level, format=log_format, stream=sys.stderr)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
import sys
|
|
3
|
+
if sys.version_info < (3, 13):
|
|
4
|
+
raise RuntimeError("At least Python 3.13 is required")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import immutables
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from abc import ABC
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from dataclasses import asdict, dataclass
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from functools import lru_cache
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Generic, Optional, TypeVar, cast
|
|
20
|
+
from yaml import Dumper, SafeDumper, add_multi_representer, add_representer, safe_dump, safe_load
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
UTC_PATTERN = re.compile(r"\+00:00$")
|
|
24
|
+
YAML_MAPPING = "tag:yaml.org,2002:map"
|
|
25
|
+
T = TypeVar("T")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@lru_cache(maxsize=100)
|
|
29
|
+
def isoformat(dt: datetime) -> str:
|
|
30
|
+
return UTC_PATTERN.sub("Z", dt.isoformat())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def to_json(obj: Any) -> Any:
|
|
34
|
+
"""
|
|
35
|
+
Convert the provided object to an object type suitable for JSON. Adds support for
|
|
36
|
+
immutables.Map, ConfigBase dataclasses, datetimes, and other iterables.
|
|
37
|
+
|
|
38
|
+
Used as the default parameter to json.dump() and json.dumps().
|
|
39
|
+
"""
|
|
40
|
+
if isinstance(obj, immutables.Map):
|
|
41
|
+
return dict(obj)
|
|
42
|
+
|
|
43
|
+
if hasattr(obj, "to_json"):
|
|
44
|
+
return obj.to_json()
|
|
45
|
+
|
|
46
|
+
if isinstance(obj, datetime):
|
|
47
|
+
return isoformat(obj)
|
|
48
|
+
|
|
49
|
+
if hasattr(obj, "__iter__"):
|
|
50
|
+
return list(obj)
|
|
51
|
+
|
|
52
|
+
raise TypeError(f"Unsupported type: {obj}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def to_yaml(dumper: Dumper | SafeDumper, obj: Any) -> Any:
|
|
56
|
+
"""
|
|
57
|
+
Convert the provided object to a representation suitable for YAML via a yaml.Dumper or
|
|
58
|
+
yaml.SafeDumper. Adds support for immutables.Map, dict, ConfigBase, Enum, datetime,
|
|
59
|
+
and other iterables.
|
|
60
|
+
"""
|
|
61
|
+
if isinstance(obj, (immutables.Map, dict)):
|
|
62
|
+
return dumper.represent_mapping(YAML_MAPPING, obj)
|
|
63
|
+
|
|
64
|
+
if isinstance(obj, ConfigBase):
|
|
65
|
+
return dumper.represent_mapping(YAML_MAPPING, obj.to_json())
|
|
66
|
+
|
|
67
|
+
if isinstance(obj, Enum):
|
|
68
|
+
return dumper.represent_str(obj.name)
|
|
69
|
+
|
|
70
|
+
if isinstance(obj, datetime):
|
|
71
|
+
return dumper.represent_str(isoformat(obj))
|
|
72
|
+
|
|
73
|
+
if hasattr(obj, "__iter__"):
|
|
74
|
+
return dumper.represent_list(list(obj))
|
|
75
|
+
|
|
76
|
+
raise TypeError(f"Unsupported type: {obj}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class DumpFormat(str, Enum):
|
|
80
|
+
"Supported dump formats for ConfigBase."
|
|
81
|
+
json = "json"
|
|
82
|
+
yaml = "yaml"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class ConfigBase(ABC, Generic[T]):
|
|
87
|
+
"Base dataclass for the various configuration dataclasses."
|
|
88
|
+
@classmethod
|
|
89
|
+
def of(cls, src: Mapping[str, Any]) -> "T":
|
|
90
|
+
"""
|
|
91
|
+
Convert the provided mapping into a ConfigBase-derived dataclass by passing the mapping's
|
|
92
|
+
items in as kwargs to said dataclass.
|
|
93
|
+
"""
|
|
94
|
+
return cast(T, cls(**src))
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_file(cls, path: Path) -> "T":
|
|
98
|
+
"Convert the provided YAML file to a ConfigBase-derived dataclass via of()."
|
|
99
|
+
with path.open("rt") as inf:
|
|
100
|
+
document = safe_load(inf)
|
|
101
|
+
|
|
102
|
+
return cls.of(
|
|
103
|
+
document if document else {}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def of_many(cls, src: Mapping[str, Mapping[str, Any]]) -> Mapping[str, "T"]:
|
|
108
|
+
"""
|
|
109
|
+
Convert the provided mapping of key=>mapping to an immutables mapping of
|
|
110
|
+
key=>dataclass via of().
|
|
111
|
+
"""
|
|
112
|
+
return immutables.Map((
|
|
113
|
+
(name, cls.of(value))
|
|
114
|
+
for name, value in src.items()
|
|
115
|
+
))
|
|
116
|
+
|
|
117
|
+
def to_json(self) -> Mapping[str, Any]:
|
|
118
|
+
"""
|
|
119
|
+
Convert this dataclass instance to a mapping suitable for JSON serialization via
|
|
120
|
+
dataclasses.asdict().
|
|
121
|
+
"""
|
|
122
|
+
return asdict(self)
|
|
123
|
+
|
|
124
|
+
def dumps(self, *args: Any, format: DumpFormat = DumpFormat.json, **kwargs: Any) -> str:
|
|
125
|
+
"Convert this dataclass instance to either a JSON string or a YAML string."
|
|
126
|
+
match format:
|
|
127
|
+
case DumpFormat.json:
|
|
128
|
+
kwargs.setdefault("default", to_json)
|
|
129
|
+
return json.dumps(self, *args, **kwargs)
|
|
130
|
+
|
|
131
|
+
case DumpFormat.yaml:
|
|
132
|
+
return safe_dump(self, *args, **kwargs)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class LogLevel(int, Enum):
|
|
136
|
+
"Supported log levels."
|
|
137
|
+
debug = logging.DEBUG
|
|
138
|
+
info = logging.INFO
|
|
139
|
+
warning = logging.WARNING
|
|
140
|
+
error = logging.ERROR
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def of(cls, value: str | int) -> "LogLevel":
|
|
144
|
+
"""
|
|
145
|
+
Create a LogLevel object from either a string or an integer.
|
|
146
|
+
|
|
147
|
+
* In the string case, the valid values are "debug", "info", "warning", and "error".
|
|
148
|
+
* In the integer case, the value corresponds to the logging level constants.
|
|
149
|
+
"""
|
|
150
|
+
for _key, _value in cls.__members__.items():
|
|
151
|
+
match (_key, _value, value):
|
|
152
|
+
case (_, int(x), int(y)) if x == y:
|
|
153
|
+
return x
|
|
154
|
+
case (str(x), y, str(z)) if x == z.lower():
|
|
155
|
+
return y
|
|
156
|
+
case _:
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
raise ValueError(f"Unknown log level: {repr(value)}")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass(frozen=True)
|
|
163
|
+
class Logging(ConfigBase["Logging"]):
|
|
164
|
+
"Logging configuration."
|
|
165
|
+
level: LogLevel = LogLevel.info
|
|
166
|
+
log_format: str = "[%(asctime)s] [%(levelname)s] [%(module)s] [%(processName)s/%(threadName)s] [%(filename)s:%(lineno)d %(funcName)s] %(message)s" # noqa
|
|
167
|
+
log_file: Optional[Path] = None
|
|
168
|
+
|
|
169
|
+
def configure(self, level_override: Optional[int] = None) -> None:
|
|
170
|
+
"Configure the Python logging system via basicConfig()."
|
|
171
|
+
args: dict[str, Any] = {
|
|
172
|
+
"level": (
|
|
173
|
+
level_override
|
|
174
|
+
if level_override is not None
|
|
175
|
+
else self.level
|
|
176
|
+
),
|
|
177
|
+
"format": self.log_format,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if self.log_file:
|
|
181
|
+
args["filename"] = self.log_file
|
|
182
|
+
else:
|
|
183
|
+
args["stream"] = sys.stderr
|
|
184
|
+
|
|
185
|
+
logging.basicConfig(**args)
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def of(cls, src: Mapping[str, Any]) -> "Logging":
|
|
189
|
+
"Create a Logging object from a mapping."
|
|
190
|
+
args: dict[str, Any] = {}
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
args["level"] = LogLevel.of(src["level"])
|
|
194
|
+
except KeyError:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
args["log_format"] = src["format"]
|
|
199
|
+
except KeyError:
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
args["log_file"] = Path(src["file"])
|
|
204
|
+
except KeyError:
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
return cls(**args)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
add_multi_representer(ConfigBase, to_yaml, Dumper=SafeDumper)
|
|
211
|
+
add_multi_representer(immutables.Map, to_yaml, Dumper=SafeDumper)
|
|
212
|
+
add_multi_representer(frozenset, to_yaml, Dumper=SafeDumper)
|
|
File without changes
|
|
@@ -16,6 +16,10 @@ workercommon.egg-info/dependency_links.txt
|
|
|
16
16
|
workercommon.egg-info/not-zip-safe
|
|
17
17
|
workercommon.egg-info/requires.txt
|
|
18
18
|
workercommon.egg-info/top_level.txt
|
|
19
|
+
workercommon/config/__init__.py
|
|
20
|
+
workercommon/config/_configparser.py
|
|
21
|
+
workercommon/config/_yaml.py
|
|
22
|
+
workercommon/config/py.typed
|
|
19
23
|
workercommon/database/__init__.py
|
|
20
24
|
workercommon/database/base.py
|
|
21
25
|
workercommon/database/pg.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workercommon-0.5.2/workercommon/database → workercommon-0.5.3/workercommon/config}/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|