socketmap-sql 0.1.0__py3-none-any.whl → 0.2.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.
@@ -1,36 +1,41 @@
1
- Metadata-Version: 2.0
1
+ Metadata-Version: 2.4
2
2
  Name: socketmap-sql
3
- Version: 0.1.0
4
- Summary: A socketmap script for SQL databases
5
- Home-page: https://github.com/kgaughan/socketmap-sql/
6
- Author: Keith Gaughan
7
- Author-email: k@stereochro.me
8
- License: MIT
9
- Platform: UNKNOWN
3
+ Version: 0.2.1
4
+ Summary: An implementation of the sendmail socketmap protocol to allow an SQL database
5
+ Author-email: Keith Gaughan <k@stereochro.me>
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/x-rst
8
+ License-Expression: MIT
10
9
  Classifier: Development Status :: 4 - Beta
11
10
  Classifier: Intended Audience :: System Administrators
12
- Classifier: License :: OSI Approved :: MIT License
13
11
  Classifier: Operating System :: POSIX
14
12
  Classifier: Programming Language :: Python :: 3
15
13
  Classifier: Programming Language :: Python :: 3 :: Only
16
14
  Classifier: Topic :: Communications :: Email :: Mail Transport Agents
17
15
  Classifier: Topic :: Database
16
+ License-File: LICENSE
17
+ Project-URL: Source, https://github.com/kgaughan/socketmap-sql
18
18
 
19
19
  =============
20
20
  socketmap-sql
21
21
  =============
22
22
 
23
- A socketmap script for interfacing with an SQL database.
23
+ A socketmap_ script for interfacing with an SQL database.
24
+
25
+ .. _socketmap: http://www.postfix.org/socketmap_table.5.html
24
26
 
25
27
  Why?
26
28
  ====
27
29
 
28
- I have a number of FreeBSD servers, with one intended to act as my primary
30
+ I have a number of FreeBSD_ servers, with one intended to act as my primary
29
31
  mailserver (the current one and remaining one becoming secondary mailservers).
30
- The problem is that I'm using postfix, and trying very hard to stick to
31
- precompiled packages rather than using ports, and postfix on FreeBSD lacks
32
+ The problem is that I'm using Postfix_, and trying very hard to stick to
33
+ precompiled packages rather than using ports, and Postfix on FreeBSD lacks
32
34
  bindings to databases. In my case, I wanted to be able to use an SQL database.
33
35
 
36
+ .. _FreeBSD: https://www.freebsd.org/
37
+ .. _Postfix: http://www.postfix.org/
38
+
34
39
  Configuration format
35
40
  ====================
36
41
 
@@ -47,7 +52,7 @@ function.
47
52
  driver = sqlite3
48
53
  database = /path/to/sqlite.db
49
54
 
50
- Other sections start wth ``table:``, and denote virtual tables to be queried.
55
+ Other sections start with ``table:``, and denote virtual tables to be queried.
51
56
  There are two fields: *transform* (optional) and *query* (required).
52
57
 
53
58
  The *transform* field gives the name of a transformation to apply to the query
@@ -88,9 +93,7 @@ following to ``master.cf``::
88
93
  Compatibility
89
94
  =============
90
95
 
91
- The script only works on Python 3.3+, though it can be made to work on Python
92
- 2.7 with some moderate patching.
96
+ The script only works on Python 3.8+.
93
97
 
94
98
  .. vim:set ft=rst:
95
99
 
96
-
@@ -0,0 +1,6 @@
1
+ socketmapsql.py,sha256=ZcApgWGIGUZ1a81uCyhMUm-evcmrzxyHAlwjs_tpNB0,7316
2
+ socketmap_sql-0.2.1.dist-info/entry_points.txt,sha256=1N1u8xbsj9ryvD0v-HWBMxNfiCarn1Dq7pXXwjn7-8E,51
3
+ socketmap_sql-0.2.1.dist-info/licenses/LICENSE,sha256=Du-PK9kF9TBcjQfRGArxGPH9HVDUxifjzF5yFex2NKE,1070
4
+ socketmap_sql-0.2.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
5
+ socketmap_sql-0.2.1.dist-info/METADATA,sha256=iN1lE_1ACmwCwW3i1xaoczFdFsLa7lXNtVcZlvlKKOs,3229
6
+ socketmap_sql-0.2.1.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.29.0)
2
+ Generator: flit 3.12.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ socketmap-sql=socketmapsql:main
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Keith Gaughan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
socketmapsql.py ADDED
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ An implementation of the sendmail socketmap protocol to allow an SQL database
4
+ to be queried out of process.
5
+ """
6
+
7
+ import argparse
8
+ import configparser
9
+ import contextlib
10
+ import dataclasses
11
+ import importlib
12
+ import os.path
13
+ import re
14
+ import select
15
+ import subprocess
16
+ import sys
17
+ import typing as t
18
+
19
+ __version__: str = "0.2.1"
20
+
21
+
22
+ Transform = t.Callable[[str, t.Mapping[str, str]], list[str]]
23
+
24
+
25
+ @dataclasses.dataclass
26
+ class TableCfg:
27
+ query: str
28
+ transform: Transform
29
+
30
+
31
+ class Cfg(t.TypedDict):
32
+ database: dict[str, str]
33
+ misc: dict[str, str]
34
+ tables: dict[str, TableCfg]
35
+
36
+
37
+ FUNC_REF_PATTERN = re.compile(
38
+ r"""
39
+ ^
40
+ (?P<module>[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*)
41
+ :
42
+ (?P<object>[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*)
43
+ $
44
+ """,
45
+ re.IGNORECASE | re.VERBOSE,
46
+ )
47
+
48
+
49
+ def match(name: str) -> tuple[str, str]:
50
+ matches = FUNC_REF_PATTERN.match(name)
51
+ if not matches:
52
+ raise ValueError(f"Malformed callable '{name}'")
53
+ return matches.group("module"), matches.group("object")
54
+
55
+
56
+ def resolve(module_name: str, obj_name: str) -> t.Callable:
57
+ """
58
+ Resolve a named object in a module.
59
+ """
60
+ return getattr(importlib.import_module(module_name), obj_name)
61
+
62
+
63
+ class MalformedNetstringError(Exception):
64
+ pass
65
+
66
+
67
+ def read_netstring(fp: t.IO[str]) -> str | None:
68
+ """Reads a single netstring."""
69
+ ns = ""
70
+ while True:
71
+ c = fp.read(1)
72
+ if c == "":
73
+ return None
74
+ if c == ":":
75
+ break
76
+ if len(ns) > 10: # noqa: PLR2004
77
+ raise MalformedNetstringError
78
+ if c == "0" and ns == "":
79
+ # We can't allow leading zeros.
80
+ if fp.read(1) != ":":
81
+ raise MalformedNetstringError
82
+ ns = c
83
+ break
84
+ ns += c
85
+ n = int(ns, 10)
86
+ result = ""
87
+ while n > 0:
88
+ segment = fp.read(n)
89
+ if segment == "":
90
+ raise MalformedNetstringError
91
+ n -= len(segment)
92
+ result += segment
93
+ if fp.read(1) != ",":
94
+ raise MalformedNetstringError
95
+ return result
96
+
97
+
98
+ def write_netstring(fp: t.IO[str], response: str) -> None:
99
+ fp.write(f"{len(response)}:{response},")
100
+ fp.flush()
101
+
102
+
103
+ def process_local(local_part: str, cfg: t.Mapping[str, str]) -> str:
104
+ delimiter = cfg.get("recipient_delimiter", "").strip()
105
+ if delimiter != "":
106
+ local_part = local_part.split(delimiter, 1)[0]
107
+ return local_part.lower()
108
+
109
+
110
+ def split(arg: str, cfg: t.Mapping[str, str]) -> list[str]:
111
+ parts = arg.split("@", 1)
112
+ parts[0] = process_local(parts[0], cfg)
113
+ parts[1] = parts[1].lower()
114
+ return parts
115
+
116
+
117
+ def parse_config(fp: t.IO[str]) -> Cfg:
118
+ transforms = {
119
+ "all": lambda arg, _: [arg],
120
+ "lowercase": lambda arg, _: [arg.lower()],
121
+ "local": lambda arg, cfg: [process_local(arg.split("@", 1)[0], cfg)],
122
+ "domain": lambda arg, _: [arg.split("@", 1)[1].lower()],
123
+ "split": split,
124
+ }
125
+
126
+ cp = configparser.ConfigParser(interpolation=None)
127
+ cp.read_file(fp)
128
+
129
+ tables = {}
130
+ for section in cp.sections():
131
+ if section.startswith("table:"):
132
+ _, table_name = section.split(":", 1)
133
+ if not cp.has_option(section, "query"):
134
+ continue
135
+
136
+ try:
137
+ transform_name = cp.get(section, "transform")
138
+ except configparser.NoOptionError:
139
+ transform_name = "all"
140
+ transform = transforms.get(transform_name)
141
+ if transform is None:
142
+ transform = resolve(*match(transform_name))
143
+
144
+ tables[table_name] = TableCfg(
145
+ query=cp.get(section, "query"),
146
+ transform=transform,
147
+ )
148
+
149
+ return Cfg(
150
+ database=dict(cp.items("database")),
151
+ tables=tables,
152
+ misc=dict(cp.items("misc")),
153
+ )
154
+
155
+
156
+ def get_int(cfg: t.Mapping[str, str], key: str) -> int | None:
157
+ return int(cfg[key]) if key in cfg else None
158
+
159
+
160
+ def serve_client(
161
+ fh_in: t.IO[str],
162
+ fh_out: t.IO[str],
163
+ conn,
164
+ timeout: int,
165
+ tables: t.Mapping[str, TableCfg],
166
+ cfg: t.Mapping[str, str],
167
+ ) -> None:
168
+ max_requests = get_int(cfg, "max_requests")
169
+ try:
170
+ while True:
171
+ if max_requests is not None:
172
+ max_requests -= 1
173
+ if max_requests < 1:
174
+ break
175
+
176
+ # Wait a short period before exiting.
177
+ iready, _, _ = select.select([fh_in], (), (), timeout)
178
+ if len(iready) == 0:
179
+ break
180
+
181
+ request = read_netstring(fh_in)
182
+ if request is None:
183
+ break
184
+
185
+ table_name, arg = request.split(" ", 1)
186
+
187
+ table = tables.get(table_name)
188
+ if table is None:
189
+ write_netstring(fh_out, f"PERM no such table: {table_name}")
190
+ break
191
+
192
+ cur = conn.cursor()
193
+ try:
194
+ cur.execute(table.query, table.transform(arg, cfg))
195
+ result = cur.fetchone()
196
+ finally:
197
+ cur.close()
198
+
199
+ if result is None:
200
+ write_netstring(fh_out, "NOTFOUND ")
201
+ else:
202
+ write_netstring(fh_out, f"OK {result[0]!s}")
203
+ except MalformedNetstringError:
204
+ write_netstring(fh_out, "PERM malformed netstring")
205
+ except Exception as exc:
206
+ write_netstring(fh_out, f"PERM {exc!s}")
207
+
208
+
209
+ def connect(settings: dict[str, str]):
210
+ """Connect to a database."""
211
+ driver = importlib.import_module(settings.pop("driver", "sqlite3"))
212
+ return driver.connect(**settings)
213
+
214
+
215
+ def make_parser() -> argparse.ArgumentParser:
216
+ parser = argparse.ArgumentParser(description="Database socketmap daemon.")
217
+ parser.add_argument(
218
+ "--config",
219
+ help="Path to config file",
220
+ type=argparse.FileType(),
221
+ default="/etc/socketmap-sql.ini",
222
+ )
223
+ parser.add_argument(
224
+ "--timeout",
225
+ help="Number of seconds to wait before exiting",
226
+ type=int,
227
+ default=5,
228
+ )
229
+ parser.add_argument(
230
+ "--client",
231
+ action="store_true",
232
+ help="Start with a debug client",
233
+ )
234
+ return parser
235
+
236
+
237
+ def main() -> int:
238
+ args = make_parser().parse_args()
239
+
240
+ with contextlib.closing(args.config):
241
+ cfg = parse_config(args.config)
242
+
243
+ if args.client:
244
+ svr_args = [arg for arg in sys.argv if arg != "--client"]
245
+ svr_args[0] = os.path.abspath(svr_args[0])
246
+ proc = subprocess.Popen(
247
+ svr_args,
248
+ stdin=subprocess.PIPE,
249
+ stdout=subprocess.PIPE,
250
+ encoding="utf-8",
251
+ )
252
+ print("Type '.exit' to exit.")
253
+ while proc.poll() is None:
254
+ req = input("socketmap> ")
255
+ if req == ".exit":
256
+ proc.terminate()
257
+ break
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))
261
+ else:
262
+ with contextlib.closing(connect(cfg["database"])) as conn:
263
+ serve_client(
264
+ sys.stdin,
265
+ sys.stdout,
266
+ conn,
267
+ args.timeout,
268
+ cfg["tables"],
269
+ cfg["misc"],
270
+ )
271
+
272
+ return 0
273
+
274
+
275
+ if __name__ == "__main__":
276
+ sys.exit(main())
277
+
278
+ # vim:set ft=python et:
@@ -1,231 +0,0 @@
1
- #!python
2
- #
3
- # socketmap-sql
4
- # by Keith Gaughan (https://github.com/kgaughan/)
5
- #
6
- # This implements the sendmail socketmap protocol to allow an SQL database to
7
- # be queried out of process.
8
- #
9
- # Copyright (c) Keith Gaughan, 2017. See 'LICENSE' for license details.
10
- #
11
-
12
- from __future__ import print_function
13
-
14
- import argparse
15
- import contextlib
16
- try:
17
- import configparser
18
- except ImportError:
19
- import ConfigParser as configparser
20
- import importlib
21
- import os.path
22
- import re
23
- import select
24
- import subprocess
25
- import sys
26
-
27
-
28
- FUNC_REF_PATTERN = re.compile(r"""
29
- ^
30
- (?P<module>
31
- [a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*
32
- )
33
- :
34
- (?P<object>
35
- [a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*
36
- )
37
- $
38
- """, re.I | re.X)
39
-
40
-
41
- def match(name):
42
- matches = FUNC_REF_PATTERN.match(name)
43
- if not matches:
44
- raise ValueError("Malformed callable '{}'".format(name))
45
- return matches.group('module'), matches.group('object')
46
-
47
-
48
- def resolve(module_name, obj_name):
49
- """
50
- Resolve a named object in a module.
51
- """
52
- return getattr(importlib.import_module(module_name), obj_name)
53
-
54
-
55
- class MalformedNetstringError(Exception):
56
- pass
57
-
58
-
59
- def read_netstring(fp):
60
- """
61
- Reads a single netstring.
62
- """
63
- n = ""
64
- while True:
65
- c = fp.read(1)
66
- if c == '':
67
- return None
68
- if c == ':':
69
- break
70
- if len(n) > 10:
71
- raise MalformedNetstringError
72
- if c == '0' and n == '':
73
- # We can't allow leading zeros.
74
- if fp.read(1) != ':':
75
- raise MalformedNetstringError
76
- n = c
77
- break
78
- n += c
79
- n = int(n, 10)
80
- result = ""
81
- while n > 0:
82
- segment = fp.read(n)
83
- n -= len(segment)
84
- result += segment
85
- if fp.read(1) != ',':
86
- raise MalformedNetstringError
87
- return result
88
-
89
-
90
- def write_netstring(fp, response):
91
- fp.write('{}:{},'.format(len(response), response))
92
- fp.flush()
93
-
94
-
95
- def parse_config(fp):
96
- def passthrough(arg):
97
- return [arg]
98
-
99
- def local_part(arg):
100
- return [arg.split('@', 1)[0]]
101
-
102
- def domain_part(arg):
103
- return [arg.split('@', 1)[1]]
104
-
105
- def split(arg):
106
- return arg.split('@', 1)
107
-
108
- cp = configparser.RawConfigParser()
109
- cp.readfp(fp)
110
-
111
- result = {
112
- 'db': dict(cp.items('database')),
113
- 'tables': {},
114
- }
115
-
116
- for section in cp.sections():
117
- if section.startswith('table:'):
118
- _, table_name = section.split(':', 1)
119
- if not cp.has_option(section, 'query'):
120
- continue
121
-
122
- try:
123
- transform_name = cp.get(section, 'transform')
124
- except configparser.NoOptionError:
125
- transform_name = 'all'
126
- if transform_name == 'all':
127
- transform = passthrough
128
- elif transform_name == 'local':
129
- transform = local_part
130
- elif transform_name == 'domain':
131
- transform = domain_part
132
- elif transform_name == 'split':
133
- transform = split
134
- else:
135
- transform = resolve(*match(transform_name))
136
-
137
- result['tables'][table_name] = {
138
- 'transform': transform,
139
- 'query': cp.get(section, 'query'),
140
- }
141
-
142
- return result
143
-
144
-
145
- def serve_client(fh_in, fh_out, conn, timeout, tables):
146
- try:
147
- while True:
148
- # Wait a short period before exiting.
149
- iready, _, _ = select.select([fh_in], (), (), timeout)
150
- if len(iready) == 0:
151
- break
152
-
153
- request = read_netstring(fh_in)
154
-
155
- table_name, arg = request.split(' ', 1)
156
-
157
- table = tables.get(table_name)
158
- if table is None:
159
- write_netstring(fh_out,
160
- 'PERM no such table: ' + table_name)
161
- continue
162
-
163
- cur = conn.cursor()
164
- try:
165
- cur.execute(table['query'], table['transform'](arg))
166
- result = cur.fetchone()
167
- finally:
168
- cur.close()
169
-
170
- if result is None:
171
- write_netstring(fh_out, 'NOTFOUND ')
172
- else:
173
- write_netstring(fh_out, 'OK ' + str(result[0]))
174
- except MalformedNetstringError:
175
- write_netstring(fh_out, 'PERM malformed netstring')
176
- except Exception as exc:
177
- write_netstring(fh_out, 'PERM ' + str(exc))
178
-
179
-
180
- def connect(settings):
181
- """
182
- Connect to a database.
183
- """
184
- driver = importlib.import_module(settings.pop('driver', 'sqlite3'))
185
- return driver.connect(**settings)
186
-
187
-
188
- def main():
189
- parser = argparse.ArgumentParser(description='Database socketmap daemon.')
190
- parser.add_argument('--config',
191
- help='Path to config file',
192
- type=argparse.FileType(),
193
- default='/etc/socketmap-sql.ini')
194
- parser.add_argument('--timeout',
195
- help='Number of seconds to wait before exiting',
196
- type=int,
197
- default=2)
198
- parser.add_argument('--client', action='store_true',
199
- help='Start with a debug client')
200
- args = parser.parse_args()
201
-
202
- with contextlib.closing(args.config):
203
- cfg = parse_config(args.config)
204
-
205
- if args.client:
206
- svr_args = [arg for arg in sys.argv if arg != '--client']
207
- svr_args[0] = os.path.abspath(svr_args[0])
208
- proc = subprocess.Popen(svr_args,
209
- stdin=subprocess.PIPE,
210
- stdout=subprocess.PIPE,
211
- encoding='utf-8')
212
- print("Type '.exit' to exit.")
213
- while proc.poll() is None:
214
- req = input('socketmap> ')
215
- if req == '.exit':
216
- proc.terminate()
217
- break
218
- write_netstring(proc.stdin, req)
219
- print(read_netstring(proc.stdout))
220
- else:
221
- with contextlib.closing(connect(cfg['db'])) as conn:
222
- serve_client(sys.stdin, sys.stdout,
223
- conn, args.timeout, cfg['tables'])
224
-
225
- return 0
226
-
227
-
228
- if __name__ == '__main__':
229
- sys.exit(main())
230
-
231
- # vim:set ft=python et:
@@ -1,78 +0,0 @@
1
- =============
2
- socketmap-sql
3
- =============
4
-
5
- A socketmap script for interfacing with an SQL database.
6
-
7
- Why?
8
- ====
9
-
10
- I have a number of FreeBSD servers, with one intended to act as my primary
11
- mailserver (the current one and remaining one becoming secondary mailservers).
12
- The problem is that I'm using postfix, and trying very hard to stick to
13
- precompiled packages rather than using ports, and postfix on FreeBSD lacks
14
- bindings to databases. In my case, I wanted to be able to use an SQL database.
15
-
16
- Configuration format
17
- ====================
18
-
19
- Configuration files are INI files containing two types of section.
20
-
21
- First is the ``[database]`` section, which gives database connection details.
22
- The *driver* field specifies the driver to use; if omitted, its value defaults
23
- to *sqlite3*. The remaining fields are passed to the driver's ``connect()``
24
- function.
25
-
26
- ::
27
-
28
- [database]
29
- driver = sqlite3
30
- database = /path/to/sqlite.db
31
-
32
- Other sections start wth ``table:``, and denote virtual tables to be queried.
33
- There are two fields: *transform* (optional) and *query* (required).
34
-
35
- The *transform* field gives the name of a transformation to apply to the query
36
- parameter before its use in the query the query. The default is to accept the
37
- parameter as-is (*all*). Other values can be a reference to a Python function
38
- in the form 'module:function', *local* for just the local part, *domain* for
39
- the domain part of the address, and *split* breaks an email address in two.
40
- It must return a list or tuple giving the postitional arguments to use in the
41
- query.
42
-
43
- The *query* field give an SQL query to be used to generate the synthetic table.
44
- Use placeholders as specified by the database driver's documentation.
45
-
46
- Usage
47
- =====
48
-
49
- Run with::
50
-
51
- socketmap-sql --config /path/to/config.ini
52
-
53
- If you don't provide the *--config* flag, it defaults to
54
- ``/etc/socketmap-sql.ini``.
55
-
56
- Postfix
57
- =======
58
-
59
- This script is intended to be executed by Postfix's spawn_ mechanism, meaning
60
- it reads its input and writes its output to stdin and stdout respectively.
61
-
62
- .. _spawn: http://www.postfix.org/spawn.8.html
63
-
64
- Assuming you've installed the script in ``/usr/local/libexec``, add the
65
- following to ``master.cf``::
66
-
67
- sockmapd unix - - n - 1 spawn
68
- user=nobody argv=/usr/local/libexec/socketmap-sql
69
-
70
- Compatibility
71
- =============
72
-
73
- The script only works on Python 3.3+, though it can be made to work on Python
74
- 2.7 with some moderate patching.
75
-
76
- .. vim:set ft=rst:
77
-
78
-
@@ -1,7 +0,0 @@
1
- socketmap_sql-0.1.0.data/scripts/socketmap-sql,sha256=0npExfWKFA2eWUb4inEd4LrKYuP9JMUYHd08MvO3Lx8,6183
2
- socketmap_sql-0.1.0.dist-info/DESCRIPTION.rst,sha256=VjUam2HBajWb0iIyW8Mrx4u7ZAKrCULlqOXLH1OtQXQ,2460
3
- socketmap_sql-0.1.0.dist-info/METADATA,sha256=MVvI2F3PVEhFchI6lvFRnvJbjxt6V15wplIIcGmlFdA,3092
4
- socketmap_sql-0.1.0.dist-info/RECORD,,
5
- socketmap_sql-0.1.0.dist-info/WHEEL,sha256=rNo05PbNqwnXiIHFsYm0m22u4Zm6YJtugFG2THx4w3g,92
6
- socketmap_sql-0.1.0.dist-info/metadata.json,sha256=7Sk8EoCX0x_S0O6-TQ--YguY4x8gBwrMk9tYR119P8g,763
7
- socketmap_sql-0.1.0.dist-info/top_level.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
@@ -1 +0,0 @@
1
- {"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: POSIX", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Topic :: Communications :: Email :: Mail Transport Agents", "Topic :: Database"], "extensions": {"python.details": {"contacts": [{"email": "k@stereochro.me", "name": "Keith Gaughan", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/kgaughan/socketmap-sql/"}}}, "generator": "bdist_wheel (0.29.0)", "license": "MIT", "metadata_version": "2.0", "name": "socketmap-sql", "summary": "A socketmap script for SQL databases", "version": "0.1.0"}
@@ -1 +0,0 @@
1
-