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.
@@ -1,18 +1,19 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: socketmap-sql
3
- Version: 0.2.0
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.8
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
- from typing import Callable, Dict, List, Mapping, Optional, Tuple, TypedDict
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: Dict[str, str]
35
- misc: Dict[str, str]
36
- tables: Dict[str, TableCfg]
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.I | re.X,
45
+ re.IGNORECASE | re.VERBOSE,
48
46
  )
49
47
 
50
48
 
51
- def match(name: str) -> Tuple[str, 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: io.TextIOWrapper) -> Optional[str]:
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: io.TextIOWrapper, response: str):
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]) -> List[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: io.TextIOWrapper) -> Cfg:
117
+ def parse_config(fp: t.IO[str]) -> Cfg:
122
118
  transforms = {
123
- "all": lambda arg, cfg: [arg],
124
- "lowercase": lambda arg, cfg: [arg.lower()],
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, cfg: [arg.split("@", 1)[1].lower()],
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) -> Optional[int]:
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: io.TextIOWrapper,
166
- fh_out: io.TextIOWrapper,
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 {str(result[0])}")
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 {str(exc)}")
206
+ write_netstring(fh_out, f"PERM {exc!s}")
211
207
 
212
208
 
213
- def connect(settings: Dict[str, str]):
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
- write_netstring(proc.stdin, req)
265
- print(read_netstring(proc.stdout))
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(
@@ -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/
@@ -1,14 +0,0 @@
1
- .venv:
2
- python3 -m venv .venv
3
-
4
- dev: .venv
5
- .venv/bin/pip install flit
6
- .venv/bin/flit install --symlink
7
-
8
- wheel: dev
9
- .venv/bin/flit build
10
-
11
- release: wheel
12
- .venv/bin/flit publish
13
-
14
- .PHONY: dev wheel release
@@ -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