socketmap-sql 0.2.0__tar.gz → 0.2.1__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.
- {socketmap-sql-0.2.0 → socketmap_sql-0.2.1}/PKG-INFO +5 -4
- socketmap_sql-0.2.1/pyproject.toml +104 -0
- {socketmap-sql-0.2.0 → socketmap_sql-0.2.1}/socketmapsql.py +34 -39
- socketmap-sql-0.2.0/.gitignore +0 -103
- socketmap-sql-0.2.0/Makefile +0 -14
- socketmap-sql-0.2.0/examples/format.ini +0 -13
- socketmap-sql-0.2.0/pyproject.toml +0 -31
- {socketmap-sql-0.2.0 → socketmap_sql-0.2.1}/LICENSE +0 -0
- {socketmap-sql-0.2.0 → socketmap_sql-0.2.1}/README.rst +0 -0
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: socketmap-sql
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: An implementation of the sendmail socketmap protocol to allow an SQL database
|
|
5
5
|
Author-email: Keith Gaughan <k@stereochro.me>
|
|
6
|
-
Requires-Python: >=3.
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
7
|
Description-Content-Type: text/x-rst
|
|
8
|
+
License-Expression: MIT
|
|
8
9
|
Classifier: Development Status :: 4 - Beta
|
|
9
10
|
Classifier: Intended Audience :: System Administrators
|
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Classifier: Operating System :: POSIX
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
14
|
Classifier: Topic :: Communications :: Email :: Mail Transport Agents
|
|
15
15
|
Classifier: Topic :: Database
|
|
16
|
+
License-File: LICENSE
|
|
16
17
|
Project-URL: Source, https://github.com/kgaughan/socketmap-sql
|
|
17
18
|
|
|
18
19
|
=============
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#:schema https://json.schemastore.org/pyproject.json
|
|
2
|
+
|
|
3
|
+
[build-system]
|
|
4
|
+
requires = ["flit_core >=3.12.0,<4"]
|
|
5
|
+
build-backend = "flit_core.buildapi"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "socketmap-sql"
|
|
9
|
+
authors = [{ name = "Keith Gaughan", email = "k@stereochro.me" }]
|
|
10
|
+
readme = "README.rst"
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
license-files = ["LICENSE"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: System Administrators",
|
|
17
|
+
"Operating System :: POSIX",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Topic :: Communications :: Email :: Mail Transport Agents",
|
|
21
|
+
"Topic :: Database",
|
|
22
|
+
]
|
|
23
|
+
dynamic = ["version", "description"]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Source = "https://github.com/kgaughan/socketmap-sql"
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
socketmap-sql = "socketmapsql:main"
|
|
30
|
+
|
|
31
|
+
[tool.flit.module]
|
|
32
|
+
name = "socketmapsql"
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = [
|
|
36
|
+
"mypy>=1.10.1",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.mypy]
|
|
40
|
+
ignore_missing_imports = true
|
|
41
|
+
|
|
42
|
+
[tool.black]
|
|
43
|
+
line-length = 120
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
target-version = "py312"
|
|
47
|
+
line-length = 120
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint]
|
|
50
|
+
select = [
|
|
51
|
+
"A",
|
|
52
|
+
# "ANN",
|
|
53
|
+
"ARG",
|
|
54
|
+
"B",
|
|
55
|
+
"C",
|
|
56
|
+
"DTZ",
|
|
57
|
+
"E",
|
|
58
|
+
"EM",
|
|
59
|
+
"EXE",
|
|
60
|
+
"F",
|
|
61
|
+
"FBT",
|
|
62
|
+
"FLY",
|
|
63
|
+
"FURB",
|
|
64
|
+
"G",
|
|
65
|
+
"I",
|
|
66
|
+
"ICN",
|
|
67
|
+
"ISC002",
|
|
68
|
+
"LOG",
|
|
69
|
+
"N",
|
|
70
|
+
"PERF",
|
|
71
|
+
"PIE",
|
|
72
|
+
"PLC",
|
|
73
|
+
"PLE",
|
|
74
|
+
"PLR",
|
|
75
|
+
"PLW",
|
|
76
|
+
# "PT",
|
|
77
|
+
"PYI",
|
|
78
|
+
"Q",
|
|
79
|
+
"RET",
|
|
80
|
+
"RSE",
|
|
81
|
+
"RUF",
|
|
82
|
+
"S",
|
|
83
|
+
"SIM",
|
|
84
|
+
"SLOT",
|
|
85
|
+
"T",
|
|
86
|
+
"TC",
|
|
87
|
+
"TID",
|
|
88
|
+
"TRY",
|
|
89
|
+
"UP",
|
|
90
|
+
"W",
|
|
91
|
+
"YTT",
|
|
92
|
+
]
|
|
93
|
+
ignore = [
|
|
94
|
+
"EM102", # Exception must not use an f-string literal, assign to variable first
|
|
95
|
+
"PLR0913", # Too many arguments in function definition
|
|
96
|
+
"T201", # `print` found
|
|
97
|
+
"TRY003", # Avoid specifying long messages outside the exception class
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint.isort]
|
|
101
|
+
force-sort-within-sections = true
|
|
102
|
+
|
|
103
|
+
[tool.ruff.lint.flake8-tidy-imports]
|
|
104
|
+
ban-relative-imports = "all"
|
|
@@ -9,19 +9,17 @@ import configparser
|
|
|
9
9
|
import contextlib
|
|
10
10
|
import dataclasses
|
|
11
11
|
import importlib
|
|
12
|
-
import io
|
|
13
12
|
import os.path
|
|
14
13
|
import re
|
|
15
14
|
import select
|
|
16
15
|
import subprocess
|
|
17
16
|
import sys
|
|
18
|
-
|
|
17
|
+
import typing as t
|
|
19
18
|
|
|
19
|
+
__version__: str = "0.2.1"
|
|
20
20
|
|
|
21
|
-
__version__ = "0.2.0"
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
Transform = Callable[[str, Mapping[str, str]], List[str]]
|
|
22
|
+
Transform = t.Callable[[str, t.Mapping[str, str]], list[str]]
|
|
25
23
|
|
|
26
24
|
|
|
27
25
|
@dataclasses.dataclass
|
|
@@ -30,10 +28,10 @@ class TableCfg:
|
|
|
30
28
|
transform: Transform
|
|
31
29
|
|
|
32
30
|
|
|
33
|
-
class Cfg(TypedDict):
|
|
34
|
-
database:
|
|
35
|
-
misc:
|
|
36
|
-
tables:
|
|
31
|
+
class Cfg(t.TypedDict):
|
|
32
|
+
database: dict[str, str]
|
|
33
|
+
misc: dict[str, str]
|
|
34
|
+
tables: dict[str, TableCfg]
|
|
37
35
|
|
|
38
36
|
|
|
39
37
|
FUNC_REF_PATTERN = re.compile(
|
|
@@ -44,18 +42,18 @@ FUNC_REF_PATTERN = re.compile(
|
|
|
44
42
|
(?P<object>[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*)
|
|
45
43
|
$
|
|
46
44
|
""",
|
|
47
|
-
re.
|
|
45
|
+
re.IGNORECASE | re.VERBOSE,
|
|
48
46
|
)
|
|
49
47
|
|
|
50
48
|
|
|
51
|
-
def match(name: str) ->
|
|
49
|
+
def match(name: str) -> tuple[str, str]:
|
|
52
50
|
matches = FUNC_REF_PATTERN.match(name)
|
|
53
51
|
if not matches:
|
|
54
52
|
raise ValueError(f"Malformed callable '{name}'")
|
|
55
53
|
return matches.group("module"), matches.group("object")
|
|
56
54
|
|
|
57
55
|
|
|
58
|
-
def resolve(module_name: str, obj_name: str) -> Callable:
|
|
56
|
+
def resolve(module_name: str, obj_name: str) -> t.Callable:
|
|
59
57
|
"""
|
|
60
58
|
Resolve a named object in a module.
|
|
61
59
|
"""
|
|
@@ -66,10 +64,8 @@ class MalformedNetstringError(Exception):
|
|
|
66
64
|
pass
|
|
67
65
|
|
|
68
66
|
|
|
69
|
-
def read_netstring(fp:
|
|
70
|
-
"""
|
|
71
|
-
Reads a single netstring.
|
|
72
|
-
"""
|
|
67
|
+
def read_netstring(fp: t.IO[str]) -> str | None:
|
|
68
|
+
"""Reads a single netstring."""
|
|
73
69
|
ns = ""
|
|
74
70
|
while True:
|
|
75
71
|
c = fp.read(1)
|
|
@@ -77,7 +73,7 @@ def read_netstring(fp: io.TextIOWrapper) -> Optional[str]:
|
|
|
77
73
|
return None
|
|
78
74
|
if c == ":":
|
|
79
75
|
break
|
|
80
|
-
if len(ns) > 10:
|
|
76
|
+
if len(ns) > 10: # noqa: PLR2004
|
|
81
77
|
raise MalformedNetstringError
|
|
82
78
|
if c == "0" and ns == "":
|
|
83
79
|
# We can't allow leading zeros.
|
|
@@ -99,31 +95,31 @@ def read_netstring(fp: io.TextIOWrapper) -> Optional[str]:
|
|
|
99
95
|
return result
|
|
100
96
|
|
|
101
97
|
|
|
102
|
-
def write_netstring(fp:
|
|
98
|
+
def write_netstring(fp: t.IO[str], response: str) -> None:
|
|
103
99
|
fp.write(f"{len(response)}:{response},")
|
|
104
100
|
fp.flush()
|
|
105
101
|
|
|
106
102
|
|
|
107
|
-
def process_local(local_part: str, cfg: Mapping[str, str]) -> str:
|
|
103
|
+
def process_local(local_part: str, cfg: t.Mapping[str, str]) -> str:
|
|
108
104
|
delimiter = cfg.get("recipient_delimiter", "").strip()
|
|
109
105
|
if delimiter != "":
|
|
110
106
|
local_part = local_part.split(delimiter, 1)[0]
|
|
111
107
|
return local_part.lower()
|
|
112
108
|
|
|
113
109
|
|
|
114
|
-
def split(arg: str, cfg: Mapping[str, str]) ->
|
|
110
|
+
def split(arg: str, cfg: t.Mapping[str, str]) -> list[str]:
|
|
115
111
|
parts = arg.split("@", 1)
|
|
116
112
|
parts[0] = process_local(parts[0], cfg)
|
|
117
113
|
parts[1] = parts[1].lower()
|
|
118
114
|
return parts
|
|
119
115
|
|
|
120
116
|
|
|
121
|
-
def parse_config(fp:
|
|
117
|
+
def parse_config(fp: t.IO[str]) -> Cfg:
|
|
122
118
|
transforms = {
|
|
123
|
-
"all": lambda arg,
|
|
124
|
-
"lowercase": lambda arg,
|
|
119
|
+
"all": lambda arg, _: [arg],
|
|
120
|
+
"lowercase": lambda arg, _: [arg.lower()],
|
|
125
121
|
"local": lambda arg, cfg: [process_local(arg.split("@", 1)[0], cfg)],
|
|
126
|
-
"domain": lambda arg,
|
|
122
|
+
"domain": lambda arg, _: [arg.split("@", 1)[1].lower()],
|
|
127
123
|
"split": split,
|
|
128
124
|
}
|
|
129
125
|
|
|
@@ -157,18 +153,18 @@ def parse_config(fp: io.TextIOWrapper) -> Cfg:
|
|
|
157
153
|
)
|
|
158
154
|
|
|
159
155
|
|
|
160
|
-
def get_int(cfg: Mapping[str, str], key: str) ->
|
|
156
|
+
def get_int(cfg: t.Mapping[str, str], key: str) -> int | None:
|
|
161
157
|
return int(cfg[key]) if key in cfg else None
|
|
162
158
|
|
|
163
159
|
|
|
164
160
|
def serve_client(
|
|
165
|
-
fh_in:
|
|
166
|
-
fh_out:
|
|
161
|
+
fh_in: t.IO[str],
|
|
162
|
+
fh_out: t.IO[str],
|
|
167
163
|
conn,
|
|
168
164
|
timeout: int,
|
|
169
|
-
tables: Mapping[str, TableCfg],
|
|
170
|
-
cfg: Mapping[str, str],
|
|
171
|
-
):
|
|
165
|
+
tables: t.Mapping[str, TableCfg],
|
|
166
|
+
cfg: t.Mapping[str, str],
|
|
167
|
+
) -> None:
|
|
172
168
|
max_requests = get_int(cfg, "max_requests")
|
|
173
169
|
try:
|
|
174
170
|
while True:
|
|
@@ -203,17 +199,15 @@ def serve_client(
|
|
|
203
199
|
if result is None:
|
|
204
200
|
write_netstring(fh_out, "NOTFOUND ")
|
|
205
201
|
else:
|
|
206
|
-
write_netstring(fh_out, f"OK {
|
|
202
|
+
write_netstring(fh_out, f"OK {result[0]!s}")
|
|
207
203
|
except MalformedNetstringError:
|
|
208
204
|
write_netstring(fh_out, "PERM malformed netstring")
|
|
209
205
|
except Exception as exc:
|
|
210
|
-
write_netstring(fh_out, f"PERM {
|
|
206
|
+
write_netstring(fh_out, f"PERM {exc!s}")
|
|
211
207
|
|
|
212
208
|
|
|
213
|
-
def connect(settings:
|
|
214
|
-
"""
|
|
215
|
-
Connect to a database.
|
|
216
|
-
"""
|
|
209
|
+
def connect(settings: dict[str, str]):
|
|
210
|
+
"""Connect to a database."""
|
|
217
211
|
driver = importlib.import_module(settings.pop("driver", "sqlite3"))
|
|
218
212
|
return driver.connect(**settings)
|
|
219
213
|
|
|
@@ -240,7 +234,7 @@ def make_parser() -> argparse.ArgumentParser:
|
|
|
240
234
|
return parser
|
|
241
235
|
|
|
242
236
|
|
|
243
|
-
def main():
|
|
237
|
+
def main() -> int:
|
|
244
238
|
args = make_parser().parse_args()
|
|
245
239
|
|
|
246
240
|
with contextlib.closing(args.config):
|
|
@@ -261,8 +255,9 @@ def main():
|
|
|
261
255
|
if req == ".exit":
|
|
262
256
|
proc.terminate()
|
|
263
257
|
break
|
|
264
|
-
|
|
265
|
-
|
|
258
|
+
if proc.stdin is not None and proc.stdout is not None:
|
|
259
|
+
write_netstring(proc.stdin, req)
|
|
260
|
+
print(read_netstring(proc.stdout))
|
|
266
261
|
else:
|
|
267
262
|
with contextlib.closing(connect(cfg["database"])) as conn:
|
|
268
263
|
serve_client(
|
socketmap-sql-0.2.0/.gitignore
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
.*.sw?
|
|
2
|
-
|
|
3
|
-
# Byte-compiled / optimized / DLL files
|
|
4
|
-
__pycache__/
|
|
5
|
-
*.py[cod]
|
|
6
|
-
*$py.class
|
|
7
|
-
|
|
8
|
-
# C extensions
|
|
9
|
-
*.so
|
|
10
|
-
|
|
11
|
-
# Distribution / packaging
|
|
12
|
-
.Python
|
|
13
|
-
env/
|
|
14
|
-
build/
|
|
15
|
-
develop-eggs/
|
|
16
|
-
dist/
|
|
17
|
-
downloads/
|
|
18
|
-
eggs/
|
|
19
|
-
.eggs/
|
|
20
|
-
lib/
|
|
21
|
-
lib64/
|
|
22
|
-
parts/
|
|
23
|
-
sdist/
|
|
24
|
-
var/
|
|
25
|
-
wheels/
|
|
26
|
-
*.egg-info/
|
|
27
|
-
.installed.cfg
|
|
28
|
-
*.egg
|
|
29
|
-
|
|
30
|
-
# PyInstaller
|
|
31
|
-
# Usually these files are written by a python script from a template
|
|
32
|
-
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
-
*.manifest
|
|
34
|
-
*.spec
|
|
35
|
-
|
|
36
|
-
# Installer logs
|
|
37
|
-
pip-log.txt
|
|
38
|
-
pip-delete-this-directory.txt
|
|
39
|
-
|
|
40
|
-
# Unit test / coverage reports
|
|
41
|
-
htmlcov/
|
|
42
|
-
.tox/
|
|
43
|
-
.coverage
|
|
44
|
-
.coverage.*
|
|
45
|
-
.cache
|
|
46
|
-
nosetests.xml
|
|
47
|
-
coverage.xml
|
|
48
|
-
*.cover
|
|
49
|
-
.hypothesis/
|
|
50
|
-
|
|
51
|
-
# Translations
|
|
52
|
-
*.mo
|
|
53
|
-
*.pot
|
|
54
|
-
|
|
55
|
-
# Django stuff:
|
|
56
|
-
*.log
|
|
57
|
-
local_settings.py
|
|
58
|
-
|
|
59
|
-
# Flask stuff:
|
|
60
|
-
instance/
|
|
61
|
-
.webassets-cache
|
|
62
|
-
|
|
63
|
-
# Scrapy stuff:
|
|
64
|
-
.scrapy
|
|
65
|
-
|
|
66
|
-
# Sphinx documentation
|
|
67
|
-
docs/_build/
|
|
68
|
-
|
|
69
|
-
# PyBuilder
|
|
70
|
-
target/
|
|
71
|
-
|
|
72
|
-
# Jupyter Notebook
|
|
73
|
-
.ipynb_checkpoints
|
|
74
|
-
|
|
75
|
-
# pyenv
|
|
76
|
-
.python-version
|
|
77
|
-
|
|
78
|
-
# celery beat schedule file
|
|
79
|
-
celerybeat-schedule
|
|
80
|
-
|
|
81
|
-
# SageMath parsed files
|
|
82
|
-
*.sage.py
|
|
83
|
-
|
|
84
|
-
# dotenv
|
|
85
|
-
.env
|
|
86
|
-
|
|
87
|
-
# virtualenv
|
|
88
|
-
.venv
|
|
89
|
-
venv/
|
|
90
|
-
ENV/
|
|
91
|
-
|
|
92
|
-
# Spyder project settings
|
|
93
|
-
.spyderproject
|
|
94
|
-
.spyproject
|
|
95
|
-
|
|
96
|
-
# Rope project settings
|
|
97
|
-
.ropeproject
|
|
98
|
-
|
|
99
|
-
# mkdocs documentation
|
|
100
|
-
/site
|
|
101
|
-
|
|
102
|
-
# mypy
|
|
103
|
-
.mypy_cache/
|
socketmap-sql-0.2.0/Makefile
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
[database]
|
|
2
|
-
; Path of SQLite database to use
|
|
3
|
-
database = /path/to/sqlite.db
|
|
4
|
-
|
|
5
|
-
[table:foo]
|
|
6
|
-
; Function to call to transform the argument in a suitable manner.
|
|
7
|
-
transform = name.of.module:name_of_function
|
|
8
|
-
; Query to run. ? is the parameter in the prepared statement after it's gone
|
|
9
|
-
; through the transform function.
|
|
10
|
-
query =
|
|
11
|
-
SELECT bar
|
|
12
|
-
FROM foo
|
|
13
|
-
WHERE baz = ?
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["flit_core >=3.2,<4"]
|
|
3
|
-
build-backend = "flit_core.buildapi"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "socketmap-sql"
|
|
7
|
-
authors = [
|
|
8
|
-
{name = "Keith Gaughan", email = "k@stereochro.me"},
|
|
9
|
-
]
|
|
10
|
-
readme = "README.rst"
|
|
11
|
-
requires-python = ">=3.8"
|
|
12
|
-
classifiers = [
|
|
13
|
-
"Development Status :: 4 - Beta",
|
|
14
|
-
"Intended Audience :: System Administrators",
|
|
15
|
-
"License :: OSI Approved :: MIT License",
|
|
16
|
-
"Operating System :: POSIX",
|
|
17
|
-
"Programming Language :: Python :: 3",
|
|
18
|
-
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
-
"Topic :: Communications :: Email :: Mail Transport Agents",
|
|
20
|
-
"Topic :: Database",
|
|
21
|
-
]
|
|
22
|
-
dynamic = ["version", "description"]
|
|
23
|
-
|
|
24
|
-
[project.urls]
|
|
25
|
-
Source = "https://github.com/kgaughan/socketmap-sql"
|
|
26
|
-
|
|
27
|
-
[project.scripts]
|
|
28
|
-
socketmap-sql = "socketmapsql:main"
|
|
29
|
-
|
|
30
|
-
[tool.flit.module]
|
|
31
|
-
name = "socketmapsql"
|
|
File without changes
|
|
File without changes
|