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.
Files changed (28) hide show
  1. {workercommon-0.5.2 → workercommon-0.5.3}/PKG-INFO +1 -1
  2. {workercommon-0.5.2 → workercommon-0.5.3}/pyproject.toml +2 -1
  3. workercommon-0.5.3/workercommon/config/__init__.py +18 -0
  4. workercommon-0.5.3/workercommon/config/_configparser.py +81 -0
  5. workercommon-0.5.3/workercommon/config/_yaml.py +212 -0
  6. workercommon-0.5.3/workercommon/py.typed +0 -0
  7. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon.egg-info/PKG-INFO +1 -1
  8. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon.egg-info/SOURCES.txt +4 -0
  9. {workercommon-0.5.2 → workercommon-0.5.3}/LICENSE.TXT +0 -0
  10. {workercommon-0.5.2 → workercommon-0.5.3}/README.md +0 -0
  11. {workercommon-0.5.2 → workercommon-0.5.3}/setup.cfg +0 -0
  12. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/__init__.py +0 -0
  13. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/background.py +0 -0
  14. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/commands.py +0 -0
  15. {workercommon-0.5.2/workercommon/database → workercommon-0.5.3/workercommon/config}/py.typed +0 -0
  16. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/connectionpool.py +0 -0
  17. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/database/__init__.py +0 -0
  18. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/database/base.py +0 -0
  19. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/database/pg.py +0 -0
  20. {workercommon-0.5.2/workercommon → workercommon-0.5.3/workercommon/database}/py.typed +0 -0
  21. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/locking.py +0 -0
  22. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/rabbitmqueue.py +0 -0
  23. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/test.py +0 -0
  24. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon/worker.py +0 -0
  25. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon.egg-info/dependency_links.txt +0 -0
  26. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon.egg-info/not-zip-safe +0 -0
  27. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon.egg-info/requires.txt +0 -0
  28. {workercommon-0.5.2 → workercommon-0.5.3}/workercommon.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workercommon
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Support code for various projects
5
5
  Author-email: "Neil E. Hodges" <47hasbegun@gmail.com>
6
6
  License-Expression: BSD-3-Clause
@@ -18,7 +18,7 @@ dependencies = [
18
18
  "psycopg2",
19
19
  "pyyaml",
20
20
  ]
21
- version = "0.5.2"
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workercommon
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Support code for various projects
5
5
  Author-email: "Neil E. Hodges" <47hasbegun@gmail.com>
6
6
  License-Expression: BSD-3-Clause
@@ -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