reliqua 0.0.6__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.
reliqua/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ """
2
+ MIT License.
3
+
4
+ Copyright (c) 2017 Terrence Meiczinger
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+ """
24
+
25
+ from .app import Application, load_config
26
+
27
+ __all__ = ["Application", "load_config"]
reliqua/api.py ADDED
@@ -0,0 +1,196 @@
1
+ """
2
+ MIT License.
3
+
4
+ Copyright (c) 2017 Terrence Meiczinger
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+ """
24
+
25
+ import glob
26
+ import importlib
27
+ import inspect
28
+ import os
29
+ import re
30
+ import sys
31
+ import uuid
32
+
33
+ import falcon
34
+ from falcon_cors import CORS
35
+
36
+ from .auth import AuthMiddleware
37
+ from .docs import Docs
38
+ from .media_handlers import JSONHandler, TextHandler, YAMLHandler
39
+ from .openapi import OpenApi
40
+ from .resources.base import Resource
41
+ from .sphinx_parser import SphinxParser
42
+ from .swagger import Swagger
43
+
44
+
45
+ class Api(falcon.App):
46
+ """Add auto route and documentation."""
47
+
48
+ def __init__(
49
+ self,
50
+ resource_path=None,
51
+ middleware=None,
52
+ config=None,
53
+ resource_attributes=None,
54
+ info=None,
55
+ openapi=None,
56
+ **_kwargs,
57
+ ):
58
+ """
59
+ Create an API instance.
60
+
61
+ :param str resource_path: Path to the resource modules
62
+ :param list middleware: Middleware
63
+ :param dict config: API configuration parameters
64
+ :param dict resource_attributes: Parameters added as resource attributes
65
+ :param dict info: Application information
66
+ :param dict openapi: OpenAPI configuration options
67
+
68
+ :return: api instance
69
+ """
70
+ info = info or {}
71
+ openapi = openapi or {}
72
+
73
+ openapi_path = openapi["path"]
74
+ self.url = openapi["api_url"]
75
+
76
+ self.openapi = openapi
77
+ self.info = info
78
+
79
+ self.openapi["spec"] = f"{openapi_path}/openapi.json"
80
+ self.openapi["static"] = f"{openapi_path}/static"
81
+ self.resources = []
82
+ self.auth = [x for x in middleware if isinstance(x, AuthMiddleware)]
83
+ self.config = config or {}
84
+ self.resource_attributes = resource_attributes or {}
85
+
86
+ path = os.path.dirname(sys.modules[__name__].__file__)
87
+ self.openapi["file_path"] = f"{path}/swagger"
88
+ self.openapi["spec_url"] = f"{self.url}{self.openapi['spec']}"
89
+ self.openapi["static_url"] = f"{self.url}{self.openapi['static']}"
90
+
91
+ middleware = middleware or []
92
+ cors = CORS(allow_all_origins=True, allow_all_methods=True, allow_all_headers=True)
93
+ middleware.append(cors.middleware)
94
+
95
+ super().__init__(middleware=middleware)
96
+
97
+ if not resource_path:
98
+ resource_path = path + "/resources"
99
+
100
+ self.req_options.auto_parse_form_urlencoded = True
101
+ self.resource_path = resource_path
102
+
103
+ self._add_handlers()
104
+ self._load_resources()
105
+ self._parse_docstrings()
106
+ self._add_routes()
107
+ self._add_docs()
108
+
109
+ def _add_handlers(self):
110
+ extra_handlers = {
111
+ "application/yaml": YAMLHandler(),
112
+ "text/html; charset=utf-8": TextHandler(),
113
+ "text/plain; charset=utf-8": TextHandler(),
114
+ "application/json": JSONHandler(),
115
+ }
116
+
117
+ self.req_options.media_handlers.update(extra_handlers)
118
+ self.resp_options.media_handlers.update(extra_handlers)
119
+
120
+ def _load_resources(self):
121
+ resources = []
122
+ path = f"{self.resource_path}/*.py"
123
+ print(f"searching {path}")
124
+ files = glob.glob(path)
125
+
126
+ for file in files:
127
+ print(f"loading {file}")
128
+ classes = self._get_classes(file)
129
+ resources.extend(classes)
130
+
131
+ # add config to resource
132
+ self.resources = [x(app_config=self.config, **self.resource_attributes) for x in resources]
133
+
134
+ def _is_route_method(self, name, suffix):
135
+ if not name.startswith("on_"):
136
+ return None
137
+
138
+ if suffix:
139
+ return name.endswith(suffix)
140
+
141
+ return re.search(r"^on_([a-z]+)$", name)
142
+
143
+ def _parse_methods(self, resource, route, methods):
144
+ parser = SphinxParser()
145
+ for name in methods:
146
+ operation_id = f"{resource.__class__.__name__}.{name}"
147
+ action = re.search(r"on_(delete|get|patch|post|put)", name).group(1)
148
+ method = getattr(resource, name)
149
+ resource.__data__[route][action] = parser.parse(method, operation_id=operation_id)
150
+
151
+ def _parse_resource(self, resource):
152
+ for route, data in resource.__routes__.items():
153
+ resource.__data__[route] = {}
154
+ suffix = data.get("suffix", None)
155
+ methods = [x for x in dir(resource) if self._is_route_method(x, suffix)]
156
+ self._parse_methods(resource, route, methods)
157
+
158
+ def _parse_docstrings(self):
159
+ for resource in self.resources:
160
+ resource.__data__ = {}
161
+ self._parse_resource(resource)
162
+
163
+ def _get_classes(self, filename):
164
+ classes = []
165
+ module_name = str(uuid.uuid3(uuid.NAMESPACE_OID, filename))
166
+ spec = importlib.util.spec_from_file_location(module_name, filename)
167
+ module = importlib.util.module_from_spec(spec)
168
+ spec.loader.exec_module(module)
169
+
170
+ for _, c in inspect.getmembers(module, inspect.isclass):
171
+ if issubclass(c, Resource) and hasattr(c, "__routes__"):
172
+ classes.append(c)
173
+
174
+ return classes
175
+
176
+ def _add_routes(self):
177
+ for resource in self.resources:
178
+ routes = resource.__routes__
179
+ for route, kwargs in routes.items():
180
+ self.add_route(route, resource, **kwargs)
181
+
182
+ def _add_docs(self):
183
+ swagger = Swagger(
184
+ self.openapi["static_url"],
185
+ self.openapi["spec_url"],
186
+ sort=self.openapi["sort"],
187
+ highlight=self.openapi["highlight"],
188
+ )
189
+ openapi = OpenApi(**self.info, auth=self.auth)
190
+ openapi.process_resources(self.resources)
191
+ schema = openapi.schema()
192
+ print(f"adding static route {self.openapi['docs_endpoint']} {self.openapi['file_path']}")
193
+ self.add_static_route(self.openapi["static"], self.openapi["file_path"])
194
+ self.add_route(self.openapi["docs_endpoint"], swagger)
195
+ print(f"adding openapi file {self.openapi['spec']}")
196
+ self.add_route(self.openapi["spec"], Docs(schema))
reliqua/app.py ADDED
@@ -0,0 +1,169 @@
1
+ """
2
+ Reliqua Framework.
3
+
4
+ Copyright 2016-2024.
5
+ """
6
+
7
+ import configparser
8
+
9
+ from gunicorn.app.base import BaseApplication
10
+
11
+ from .api import Api
12
+ from .middleware import ProcessParams
13
+
14
+ GUNICORN_DEFAULTS = {
15
+ "bind": "127.0.0.1:8000", # Address and port to listen for requests [host:port]
16
+ "workers": 1, # Number of worker processes
17
+ "loglevel": "critical", # Log level (debug, error, info, critical)
18
+ "accesslog": "-", # Access log path ("-" for stream, None to disable)
19
+ "errorlog": "-", # Error log path ("-" for stream, None to disable)
20
+ "worker_class": "gthread", # Type of worker processes
21
+ "timeout": 30, # Inactive worker timeout (worker killed)
22
+ "keepalive": 2, # Keep alive period for a request
23
+ }
24
+
25
+ OPENAPI_DEFAULTS = {
26
+ "api_url": None, # API URL used by OpenAI UI (if different from bind, for example, behind a proxy)
27
+ "ui_url": None, # OpenAPI UI index url
28
+ "highlight": True, # Enable OpenAPI syntax highlighting
29
+ "sort": "alpha", # OpenAPI endpoint/tag sort order
30
+ "path": "/openapi", # OpenAPI endpoint path (JSON and static files)
31
+ "docs_endpoint": "/docs", # Documentation endpoint
32
+ }
33
+
34
+ INFO_DEFAULTS = {
35
+ "title": "Application Title", # Application title
36
+ "version": "1.0.0", # Application version
37
+ "description": "", # Application description
38
+ "license": "", # License
39
+ "license_url": "", # License URL
40
+ "contact_name": "", # Contact name
41
+ }
42
+
43
+
44
+ def update_dict(a, b):
45
+ """
46
+ Update dictionary A with values from dictionary B.
47
+
48
+ Update the diction A with values from dictionary B. Remove
49
+ keys from A that are not in B.
50
+
51
+ :param dict a: Dictionary A
52
+ :param dict b: Dictionary B
53
+ """
54
+ # Create a new dictionary with keys from A that are also in B
55
+ updated_dict = {key: a[key] for key in a if key in b}
56
+
57
+ # Add keys from B that are not in A
58
+ for key in b:
59
+ if key not in updated_dict:
60
+ updated_dict[key] = b[key]
61
+
62
+ return updated_dict
63
+
64
+
65
+ def load_config(config_file):
66
+ """
67
+ Load configuration file.
68
+
69
+ :param str config_file: Configuration file
70
+ :return dict: Options dictionary
71
+ """
72
+ params = {}
73
+
74
+ try:
75
+ section = "config"
76
+ config = configparser.ConfigParser()
77
+ config.read(config_file)
78
+
79
+ for option in config.options(section):
80
+ params[option] = config.get(section, option)
81
+ except TypeError:
82
+ pass
83
+
84
+ return params
85
+
86
+
87
+ class Application(BaseApplication):
88
+ """Create a standalone API application."""
89
+
90
+ def __init__(
91
+ self,
92
+ resource_path=None,
93
+ middleware=None,
94
+ config=None,
95
+ resource_attributes=None,
96
+ info=None,
97
+ openapi=None,
98
+ gunicorn=None,
99
+ **_kwargs,
100
+ ):
101
+ """
102
+ Create Application instance.
103
+
104
+ :param str resource_path: Path to the API resource modules
105
+ :param list middleware: Middleware
106
+ :param dict config: Application resource parameters (passed to resources as config attribute)
107
+ :param dict resource_attributes: Parameters added as resource attributes
108
+ :param str info: Application information
109
+ :param str openapi: OpenAPI configuration options
110
+ :param dict gunicorn: Gunicorn configuration options
111
+
112
+ :return: Application instance
113
+ """
114
+ middleware = middleware or []
115
+ gunicorn = gunicorn or {}
116
+ openapi = openapi or {}
117
+ info = info or {}
118
+
119
+ resource_path = resource_path or ""
120
+ resource_attributes = resource_attributes or {}
121
+
122
+ self.gunicorn_options = update_dict(gunicorn, GUNICORN_DEFAULTS)
123
+ openapi = update_dict(openapi, OPENAPI_DEFAULTS)
124
+ info = update_dict(info, INFO_DEFAULTS)
125
+
126
+ middleware.append(ProcessParams())
127
+
128
+ # trim slashes from proxy URL if specified; otherwise set default proxy url
129
+ bind = self.gunicorn_options["bind"]
130
+ openapi["api_url"] = openapi["api_url"].rstrip("/") if openapi["api_url"] else f"http://{bind}"
131
+
132
+ self.application = Api(
133
+ resource_path=resource_path,
134
+ middleware=middleware,
135
+ config=config,
136
+ resource_attributes=resource_attributes,
137
+ openapi=openapi,
138
+ info=info,
139
+ )
140
+
141
+ super().__init__()
142
+
143
+ def init(self, _parser, _opts, _args):
144
+ """
145
+ Init method.
146
+
147
+ This is not invoked, since the load_config method
148
+ is also overridden.
149
+ """
150
+ return
151
+
152
+ def load_config(self):
153
+ """
154
+ Load configuration.
155
+
156
+ Default config loader. This load settings from a config file
157
+ with a 'config' section. This method should be overloaded
158
+ if a custom loader is required.
159
+ """
160
+ for key, value in self.gunicorn_options.items():
161
+ self.cfg.set(key.lower(), value)
162
+
163
+ def load(self):
164
+ """
165
+ Load the application.
166
+
167
+ Base class.
168
+ """
169
+ return self.application