underscorepy 0.9.0__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.
@@ -0,0 +1,78 @@
1
+ #
2
+ # (c) 2015-2024 Matthew Shaw
3
+ #
4
+ # Authors
5
+ # =======
6
+ # Matthew Shaw <mshaw.cx@gmail.com>
7
+ #
8
+
9
+ import logging
10
+ import os
11
+
12
+ import _
13
+
14
+
15
+ class Nginx(_.supports.Support):
16
+ async def init(self, component_name, **kwds):
17
+ self.params = dict(
18
+ root = _.paths(),
19
+ ns = _.ns,
20
+ name = _.name,
21
+ web_root = '/var/www/html',
22
+ address = '127.0.0.1',
23
+ port = 8080,
24
+ listen_ip4 = '0.0.0.0',
25
+ listen_ip6 = '[::]',
26
+ listen_http = 80,
27
+ listen_ssl = 443,
28
+ conf_path = '/etc/nginx/sites-available/{server_name}',
29
+ )
30
+
31
+ self.params.update(dict(_.config[_.name]))
32
+ try:
33
+ self.params.update(dict(_.config[component_name]))
34
+ except KeyError:
35
+ pass
36
+ self.params.update(kwds)
37
+
38
+ try:
39
+ _.argparser.add_argument(f'--nginx',
40
+ metavar='<server_name>', default=0, nargs='?',
41
+ help='install nginx config'
42
+ )
43
+ except:
44
+ raise _.error('Multiple nginx supports detected: %s', component_name) from None
45
+
46
+ async def args(self, component_name):
47
+ if _.args.nginx == 0:
48
+ return
49
+
50
+ name = _.args.nginx if _.args.nginx else _.name
51
+
52
+ if 'server_name' not in self.params:
53
+ self.params['server_name'] = name
54
+
55
+ conf = _.paths(f'{name}.conf')
56
+ if not os.path.exists(conf):
57
+ conf = os.path.join(self.root, 'nginx', 'nginx.conf')
58
+ logging.debug('Loading configuration: %s', conf)
59
+
60
+ try:
61
+ with open(conf, 'r') as fp:
62
+ conf = fp.read()
63
+ try:
64
+ conf = conf.format(**self.params)
65
+ except KeyError as e:
66
+ raise _.error('Missing nginx configuration file parameter: %s', e) from None
67
+
68
+ path = self.params['conf_path'].format(**self.params)
69
+ with open(path, 'w') as fp:
70
+ fp.write(conf)
71
+ except _.error:
72
+ raise
73
+ except Exception as e:
74
+ raise _.error('Could not write nginx configuration: %s', e) from None
75
+
76
+ logging.info('Installed nginx configuration: %s', name)
77
+
78
+ _.application.stop()
@@ -0,0 +1,59 @@
1
+
2
+ upstream frontends {{
3
+ server {address}:{port};
4
+ }}
5
+
6
+ server {{
7
+ listen {listen_ip4}:{listen_http};
8
+ listen {listen_ip6}:{listen_http};
9
+ server_name {server_name};
10
+ return 301 https://$server_name$request_uri;
11
+ }}
12
+
13
+ server {{
14
+ server_name {server_name};
15
+ listen {listen_ip4}:{listen_ssl} ssl;
16
+ listen {listen_ip6}:{listen_ssl} ssl;
17
+ root {web_root};
18
+ index index.html;
19
+
20
+ ssl_certificate /etc/letsencrypt/live/{server_name}/fullchain.pem;
21
+ ssl_certificate_key /etc/letsencrypt/live/{server_name}/privkey.pem;
22
+ ssl_prefer_server_ciphers on;
23
+ ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
24
+
25
+ location ^~ /static/ {{
26
+ root /var/www;
27
+ if ($query_string) {{
28
+ expires max;
29
+ }}
30
+ }}
31
+
32
+ location = /favicon.ico {{
33
+ rewrite (.*) /static/favicon.ico;
34
+ }}
35
+
36
+ location = /robots.txt {{
37
+ rewrite (.*) /static/robots.txt;
38
+ }}
39
+
40
+ location /ws {{
41
+ proxy_http_version 1.1;
42
+ proxy_set_header Upgrade $http_upgrade;
43
+ proxy_set_header Connection "upgrade";
44
+ proxy_read_timeout 1h;
45
+ proxy_send_timeout 1h;
46
+ proxy_set_header X-Real-IP $remote_addr;
47
+ proxy_set_header X-Scheme $scheme;
48
+ proxy_pass http://frontends;
49
+ }}
50
+
51
+ location / {{
52
+ proxy_pass_header Server;
53
+ proxy_set_header Host $http_host;
54
+ proxy_redirect off;
55
+ proxy_set_header X-Real-IP $remote_addr;
56
+ proxy_set_header X-Scheme $scheme;
57
+ proxy_pass http://frontends;
58
+ }}
59
+ }}
@@ -0,0 +1,60 @@
1
+ #
2
+ # (c) 2015-2023 Matthew Shaw
3
+ #
4
+ # Authors
5
+ # =======
6
+ # Matthew Shaw <mshaw.cx@gmail.com>
7
+ #
8
+
9
+ import logging
10
+ import os
11
+ import sys
12
+
13
+ import _
14
+
15
+
16
+ class Systemd(_.supports.Support):
17
+ async def init(self, component_name, **kwds):
18
+ self.params = dict(
19
+ ns = _.ns,
20
+ name = _.name,
21
+ cmdline = f'{os.path.abspath(sys.argv[0])}',
22
+ user = os.environ.get('USER', 'nobody'),
23
+ conf_path = '/etc/systemd/system/{name}.service',
24
+ )
25
+
26
+ self.params.update(kwds)
27
+
28
+ _.argparser.add_argument(f'--systemd',
29
+ metavar='<user>', default=0, nargs='?',
30
+ help='install systemd service'
31
+ )
32
+
33
+ async def args(self, component_name):
34
+ if _.args.systemd == 0:
35
+ return
36
+
37
+ if _.args.systemd:
38
+ self.params['user'] = _.args.systemd
39
+
40
+ # set name to the value from params
41
+ name = self.params.get('name')
42
+
43
+ with open(os.path.join(self.root, 'systemd', 'systemd.service'), 'r') as fp:
44
+ conf = fp.read()
45
+
46
+ try:
47
+ conf = conf.format(**self.params)
48
+ except KeyError as e:
49
+ raise _.error('Missing systemd parameter: %s', e)
50
+
51
+ path = self.params['conf_path'].format(**self.params)
52
+ try:
53
+ with open(path, 'w') as fp:
54
+ fp.write(conf)
55
+ except Exception as e:
56
+ raise _.error('Could not write systemd service: %s: %s', name, e) from None
57
+
58
+ logging.info('Installed systemd service: %s', name)
59
+
60
+ _.application.stop()
@@ -0,0 +1,12 @@
1
+ [Unit]
2
+ Description={name}
3
+ After=network.target
4
+
5
+ [Service]
6
+ Type=simple
7
+ ExecStart={cmdline}
8
+ User={user}
9
+ Restart=on-failure
10
+
11
+ [Install]
12
+ WantedBy=multi-user.target
_/test.py ADDED
@@ -0,0 +1,20 @@
1
+ #
2
+ # (c) 2024 Matthew Shaw
3
+ #
4
+ # Authors
5
+ # =======
6
+ # Matthew Shaw <mshaw.cx@gmail.com>
7
+ #
8
+
9
+ import logging
10
+
11
+ import _
12
+
13
+
14
+ class TestApplication(_.WebApplication):
15
+ async def __listen(self, patterns, **kwds):
16
+ '''call the Tornado Application init here to give children a chance
17
+ to initialize patterns and settings'''
18
+ logging.info('Would be listening on %s:%d', _.args.address, _.args.port)
19
+
20
+ _.Application = TestApplication
_/utils.py ADDED
@@ -0,0 +1,85 @@
1
+ #
2
+ # (c) 2015-2024 Matthew Shaw
3
+ #
4
+ # Authors
5
+ # =======
6
+ # Matthew Shaw <mshaw.cx@gmail.com>
7
+ #
8
+
9
+ import asyncio
10
+ import base64
11
+ import datetime
12
+ import json
13
+ import inspect
14
+ import logging
15
+ import os
16
+ import time
17
+ import uuid
18
+
19
+ import tornado.web
20
+
21
+ import _
22
+
23
+
24
+ # alias so handlers don't have to import tornado.web directly
25
+ HTTPError = tornado.web.HTTPError
26
+
27
+ # return the current time in milliseconds
28
+ now = lambda: int(time.time() * 1000)
29
+
30
+ # add a prefix to dictionary names
31
+ prefix = lambda kwds,prepend='_': dict((f'{prepend}{k}',v) for k,v in kwds.items())
32
+
33
+ # pass _.function as a filter to the all function
34
+ function = type(lambda: None)
35
+
36
+
37
+ async def wait(result):
38
+ '''wait on coroutines or return result of non-coroutines'''
39
+ try:
40
+ return result if not asyncio.iscoroutine(result) else await result
41
+ except Exception as e:
42
+ if _.args.debug:
43
+ trace = []
44
+ for f in inspect.stack()[:0:-1]:
45
+ if not f.code_context:
46
+ continue
47
+ context = '\n'.join(c.strip() for c in f.code_context)
48
+ msg = f' File "{f.filename}", line {f.lineno}, in {f.function}\n {context}'
49
+ trace.append(msg)
50
+ logging.exception(f'Unhandled exception:\n%s', '\n'.join(trace))
51
+ raise
52
+
53
+
54
+ class Paths:
55
+ def __init__(self, root, ns=None):
56
+ self.root = root
57
+ self.ns = ns
58
+
59
+ def __call__(self, *args):
60
+ if self.ns is None:
61
+ return os.path.join(self.root, *args)
62
+ else:
63
+ return os.path.join(self.root, self.ns, *args)
64
+
65
+ # support / as a path joiner
66
+ def __truediv__(self, other):
67
+ return Paths(self(other))
68
+
69
+ def __str__(self):
70
+ return self()
71
+
72
+
73
+ class JSONEncoder(json.JSONEncoder):
74
+ def default(self, obj):
75
+ if hasattr(obj, '_as_dict'):
76
+ return obj._as_dict()
77
+ if isinstance(obj, bytes):
78
+ return base64.b64encode(obj).decode('ascii')
79
+ if isinstance(obj, uuid.UUID):
80
+ return str(obj)
81
+ if isinstance(obj, datetime.datetime):
82
+ return str(obj)
83
+ return json.JSONEncoder.default(self, obj)
84
+
85
+ json._default_encoder = JSONEncoder()
_/version.py ADDED
@@ -0,0 +1,5 @@
1
+ __author__ = 'Matthew Shaw'
2
+ __email__ = 'mshaw.cx@gmail.com'
3
+ __copyright__ = '(c)2015-2024, Matthew Shaw'
4
+ __license__ = 'MIT'
5
+ __version__ = '0.9.0'
_/websockets.py ADDED
@@ -0,0 +1,57 @@
1
+ #
2
+ # (c) 2015-2023 Matthew Shaw
3
+ #
4
+ # Authors
5
+ # =======
6
+ # Matthew Shaw <mshaw.cx@gmail.com>
7
+ #
8
+
9
+ import tornado.websocket
10
+
11
+ import _
12
+
13
+
14
+ class WebSocket(tornado.websocket.WebSocketHandler):
15
+ '''Tweak the default WebSocket behavior for more responsive behavior'''
16
+ websockets = {}
17
+
18
+ def initialize(self, websockets=None):
19
+ if websockets is not None:
20
+ self.websockets = websockets
21
+
22
+ def check_origin(self, origin):
23
+ if _.args.debug:
24
+ return True
25
+ # TODO: let app specify origin policy
26
+ return True
27
+
28
+ def open(self):
29
+ self.set_nodelay(True)
30
+ self.websockets[id(self)] = self
31
+
32
+ def on_close(self):
33
+ self.websockets.pop(id(self), None)
34
+
35
+
36
+ class Protected(WebSocket):
37
+ '''Require a valid user session to establish WebSocket connection'''
38
+
39
+ async def prepare(self):
40
+ self.session_id = self.get_signed_cookie('session_id')
41
+ if not self.session_id:
42
+ raise _.HTTPError(403)
43
+ self.session_id = self.session_id.decode('utf-8')
44
+
45
+ self.session = await _.wait(_.sessions.load_session(self.session_id))
46
+ if not self.session:
47
+ raise _.HTTPError(403)
48
+
49
+
50
+ class EchoMixin:
51
+ '''Reflect incoming messages to all other connected WebSockets'''
52
+
53
+ def on_message(self, msg):
54
+ for ws in self.websockets.values():
55
+ if ws is self:
56
+ continue
57
+ ws.write_message(msg)
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: underscorepy
3
+ Version: 0.9.0
4
+ Summary: _.py library
5
+ Author-email: Matthew Shaw <mshaw.cx@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://shaw.cx/_.py
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Software Development :: Libraries
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: tornado>=6.3
17
+ Requires-Dist: sqlalchemy[asyncio]>=2
18
+ Provides-Extra: postgres
19
+ Requires-Dist: asyncpg; extra == "postgres"
20
+ Provides-Extra: sqlite
21
+ Requires-Dist: aiosqlite; extra == "sqlite"
22
+ Provides-Extra: redis
23
+ Requires-Dist: redis>=5.0.1; extra == "redis"
24
+ Provides-Extra: protobuf
25
+ Requires-Dist: protobuf>=5.26; extra == "protobuf"
26
+ Provides-Extra: ldap
27
+ Requires-Dist: python-ldap; extra == "ldap"
28
+ Provides-Extra: migrations
29
+ Requires-Dist: alembic; extra == "migrations"
30
+ Provides-Extra: test
31
+ Requires-Dist: pytest; extra == "test"
32
+ Requires-Dist: aiosqlite; extra == "test"
33
+ Requires-Dist: alembic; extra == "test"
34
+ Provides-Extra: all
35
+ Requires-Dist: underscorepy[ldap,migrations,postgres,protobuf,redis,sqlite]; extra == "all"
36
+ Dynamic: license-file
37
+
38
+
39
+ underscore.py
40
+ -------------
41
+
42
+ ```INI
43
+ [myapp]
44
+ address = 0.0.0.0
45
+ port = 8080
46
+ ```
47
+
48
+ ```Python
49
+ import _
50
+ import _.websockets
51
+
52
+ class Echo(_.websockets.WebSocket):
53
+ def on_message(self, msg):
54
+ print('Echo:', msg)
55
+ self.write_message(msg)
56
+
57
+ class MyApp(_.Application):
58
+ def initialize(self):
59
+ self.patterns = [
60
+ ( r'/ws', Echo ),
61
+ ( r'/([a-z]*)', _.handlers.Template ),
62
+ ]
63
+
64
+ MyApp.main()
65
+ ```
66
+
67
+ Look at the skeleton directory for a more complete example application.
68
+
69
+ Schema changes
70
+ --------------
71
+
72
+ Tables for records are created when the database is empty. After that, each
73
+ start compares the models (for example your `.proto` messages) with the
74
+ database and refuses to run if they differ:
75
+
76
+ ```
77
+ ERROR Database "store" differs from the schema:
78
+ ALTER TABLE "Reports_multiple" ADD COLUMN _position INTEGER
79
+ Run with --store-migrate to apply them
80
+ ```
81
+
82
+ `--<database>-migrate` applies changes that can't lose data. Drops and type
83
+ changes are printed as SQL to run by hand. This needs `alembic`
84
+ (`pip install underscorepy[migrations]`); set `check_schema = false` in the
85
+ database's section to skip the check.
@@ -0,0 +1,39 @@
1
+ _/__init__.py,sha256=O8CiMeB2BlfNq0-ZCRXIAkmJby91V15F9EvUr_QuGQ4,907
2
+ _/application.py,sha256=9iDLHMtDk24CA4ItuNBkK1gsQjr8etkDBY6LRgu8v-o,9505
3
+ _/auth.py,sha256=7PxD7Kwr5oqNn-c4Id_xJUGKmYgvfHl9vJCdxfaT7do,5501
4
+ _/components.py,sha256=BX0rVbQvHBqjh3ge8ibc6R4S2ZM0oxFNrqwZgjrqyWk,2995
5
+ _/handlers.py,sha256=wLTwPFXbeznyQ9SoRr_uOZRvfgEikSmnCFtVaSR0Q5g,1239
6
+ _/settings.py,sha256=sw0aD2ZTzSqQoPPrgQ35PzagsN_p3Xl63cRjAF0tyvM,4594
7
+ _/test.py,sha256=0I5aeeDbnH7t5o4lytMZLOTeN8K5n6_gmOlX35tsxHk,443
8
+ _/utils.py,sha256=k8GQn6HxFFXAF4vTdmN_zYDnzLFvX4HXqNwSENWYBkc,2172
9
+ _/version.py,sha256=VhvZmSQKhnrWLCBNjnZ9Y-0MdiaQjur-7eWuAd3QVzA,159
10
+ _/websockets.py,sha256=Ayp_2IK4125V_oJnyRVeIzkqnMxJcUFizduoO9JSviU,1427
11
+ _/caches/__init__.py,sha256=6XPB4pTSj_fzXaJfbr0FV5JcKmCIhJdyS_gxHS5nHwM,1152
12
+ _/caches/dbcache.py,sha256=N_a8EOPLJ_vpaQ-oPEum_LvbrIkS0RC4Q37aUY5irRs,4953
13
+ _/caches/memory.py,sha256=HAnJcM6nyfsYaH3zXC7TpZdDjRHjqNPWj9Uywg8yUaA,2340
14
+ _/caches/redis.py,sha256=OAhzTvRq0CM3WkhNG0j5VIAD8LFIQorv6sH1x8Ktwt4,3315
15
+ _/databases/__init__.py,sha256=HpZohhzko7-Tut1uS9HapdSX1igWru7oPUykRq6D5CI,10377
16
+ _/databases/postgres.py,sha256=-7nulRmjmrp0BL-xPd2GtgiTTcAvhI6unTfJAcXOdQY,866
17
+ _/databases/sqlite.py,sha256=M_YEGh3-WRqO439ZModZzlv0yy_uw4qe7aGBdqroVzo,2213
18
+ _/logins/__init__.py,sha256=vWvFPo5e5IdKP_YTDZlWfX0C4QgvvvoziZul48qY-UE,2978
19
+ _/logins/dblogin.py,sha256=a0XsPKX0z4UE6HXd9LHMXmJSQvBFoothAOZpgyTjxUY,7597
20
+ _/logins/facebook.py,sha256=fMVLOMfwjt8-zYwi-uBnr0fRIEluLwGY7RgFRIv-Wk8,1136
21
+ _/logins/gitlab.py,sha256=VTHklbKlTiapo5L1WOzKrGKG1zrzdo7TFKNY_hXAFTk,1614
22
+ _/logins/google.py,sha256=1VM6Xexv3MFnbtzLAiihGUMv29B9MjKjysaSTpKKHN0,365
23
+ _/logins/htpasswd.py,sha256=CLZEsMs86zs5cK7EIOA3Mq6xf0b9v7Z80pTloqgoSe4,1098
24
+ _/logins/oauth2.py,sha256=z64-wDsGlDRqBZOpJhKLY58u0mEYt1KmU74P_T_RioQ,1683
25
+ _/logins/slap.py,sha256=Mnoa0SD7fbCRXHOP3OHlQcVhXhFfFAKslFyM-vj9w88,1471
26
+ _/records/Protobuf.proto,sha256=QOSY73vLWExLKhtn_bh8OYir8w5zfic6iZ-gnmQH9-s,489
27
+ _/records/__init__.py,sha256=Ec-q9Z3XsIN2yhnrb7VWf4T4HnUtJ6MwR_UBghrP_uE,5477
28
+ _/records/data.py,sha256=VsZAV1V5CsYWFqSgxD4DEz95Ywhp-U-YiLv4780wozE,8149
29
+ _/records/protobuf.py,sha256=FYr86VDXJvJ1l27Tk1ED-oIDm6Spcs5a85QPTevWAc4,18998
30
+ _/supports/__init__.py,sha256=x49qm_0Usm7EWaUeif4lctq2PwSiRmHlvtWVGPG9byg,532
31
+ _/supports/nginx/__init__.py,sha256=40utmYxt8DiJ_siVaM0WabvvkL84PLc-es7homXLn-4,2282
32
+ _/supports/nginx/nginx.conf,sha256=eZYQA8KllYyPfUE42BTmsJ3qQDzaCg53m1SZREOEZ3o,1546
33
+ _/supports/systemd/__init__.py,sha256=YIzmN1FErqiZJFzlMQoWH7DcCW5WAgmzFzSPz4axNmg,1578
34
+ _/supports/systemd/systemd.service,sha256=bmV0gogbZrmm72OiKQPFn9qQPdZM4M5yiYKtphALm74,159
35
+ underscorepy-0.9.0.dist-info/licenses/LICENSE,sha256=9W6z5DdvUR1RWcwCfZOd3JJV-bj0WMAmQxd0RiqMAUY,1085
36
+ underscorepy-0.9.0.dist-info/METADATA,sha256=eCQFKnb19hZJ33RafWVYLzC1lO9AIfGAQvIgNxVZQNY,2466
37
+ underscorepy-0.9.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
38
+ underscorepy-0.9.0.dist-info/top_level.txt,sha256=RBW342H8b2uiSSre8fJ8-wDQEF5FiBd0cM7MQhSzUoQ,2
39
+ underscorepy-0.9.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2023 Matthew Shaw
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.
22
+
@@ -0,0 +1 @@
1
+ _