Pyjoyment 0.1.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.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/URL/Template.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.URL.Template - URI Templates
|
|
3
|
+
=================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.URL.Template import Template
|
|
7
|
+
|
|
8
|
+
template = Template('https://api.github.com/repos/{owner}/{repo}/issues{?state,labels}')
|
|
9
|
+
print(template.expand(owner='dex4er', repo='Pyjoyment', state='open'))
|
|
10
|
+
print(template.match('https://api.github.com/repos/dex4er/Pyjoyment/issues?state=open'))
|
|
11
|
+
|
|
12
|
+
# Or import the module, as in Google Python Style Guide
|
|
13
|
+
|
|
14
|
+
import Pyjo.URL.Template
|
|
15
|
+
|
|
16
|
+
template = Pyjo.URL.Template.new('file:///project/{+path}')
|
|
17
|
+
print(template.expand({'path': 'src/main.py'}))
|
|
18
|
+
print(template.match('file:///project/src/main.py')['path'])
|
|
19
|
+
|
|
20
|
+
:mod:`Pyjo.URL.Template` is an implementation of the URI Templates of
|
|
21
|
+
:rfc:`6570`, of all its levels: a template is a URI with expressions in braces,
|
|
22
|
+
which :meth:`~Pyjo_URL_Template.expand` replaces with the values of their
|
|
23
|
+
variables, encoded as the operator of each expression says. ::
|
|
24
|
+
|
|
25
|
+
# "/search?q=Hello%20World%21&lang=en"
|
|
26
|
+
Pyjo.URL.Template.new('/search{?q,lang}').expand(q='Hello World!', lang='en')
|
|
27
|
+
|
|
28
|
+
:meth:`~Pyjo_URL_Template.match` is the opposite, which the RFC leaves to its
|
|
29
|
+
implementations: the values of the variables of a URI that the template
|
|
30
|
+
expands to, as a server that routes a URI by its template needs them. The URI
|
|
31
|
+
is matched from both of its ends, in a time linear in its length, so a URI of
|
|
32
|
+
a client cannot make it slow. One expression takes what the others leave: the
|
|
33
|
+
one of the reserved expansion ``{+var}``, of a fragment ``{#var}`` or with the
|
|
34
|
+
explode modifier ``{var*}``, of which a template has at most one, or else the
|
|
35
|
+
last expression of the template. Every other expression ends at the first
|
|
36
|
+
character of what follows it in the template, as ``{name}`` ends at the
|
|
37
|
+
``/`` of ``{name}/raw``. ::
|
|
38
|
+
|
|
39
|
+
# {'owner': 'dex4er', 'path': 'src/main.py'}
|
|
40
|
+
Pyjo.URL.Template.new('repo://{owner}/blob/{+path}').match('repo://dex4er/blob/src/main.py')
|
|
41
|
+
|
|
42
|
+
A template is for the URIs that a protocol gives as templates, as the resource
|
|
43
|
+
templates of the Model Context Protocol. A request of an application is
|
|
44
|
+
dispatched by its routes, whose patterns of :mod:`Pyjoyment.Routes.Pattern`
|
|
45
|
+
have placeholders with types, defaults and formats, which a template does not
|
|
46
|
+
have.
|
|
47
|
+
|
|
48
|
+
See also
|
|
49
|
+
--------
|
|
50
|
+
|
|
51
|
+
:mod:`Pyjo.URL`, :mod:`Pyjoyment.Routes.Pattern`, https://mojolicious.org.
|
|
52
|
+
|
|
53
|
+
It has no counterpart in Mojolicious, and is written from :rfc:`6570`.
|
|
54
|
+
|
|
55
|
+
Classes
|
|
56
|
+
-------
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
import Pyjo.Base
|
|
60
|
+
import Pyjo.Exception
|
|
61
|
+
import Pyjo.String.Unicode
|
|
62
|
+
|
|
63
|
+
from Pyjo.Util import decode, url_escape, url_unescape
|
|
64
|
+
|
|
65
|
+
import builtins
|
|
66
|
+
import re
|
|
67
|
+
|
|
68
|
+
from collections.abc import Mapping
|
|
69
|
+
from typing import Any, cast
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
type Anything = builtins.object
|
|
73
|
+
|
|
74
|
+
# The first character, the separator, whether the variables are named, the value of an empty named one, and whether
|
|
75
|
+
# the reserved characters stay, of each operator, as the table of the appendix A of RFC 6570 has them
|
|
76
|
+
_OPERATORS: dict[str, tuple[str, str, bool, str, bool]] = {
|
|
77
|
+
'': ('', ',', False, '', False),
|
|
78
|
+
'+': ('', ',', False, '', True),
|
|
79
|
+
'#': ('#', ',', False, '', True),
|
|
80
|
+
'.': ('.', '.', False, '', False),
|
|
81
|
+
'/': ('/', '/', False, '', False),
|
|
82
|
+
';': (';', ';', True, '', False),
|
|
83
|
+
'?': ('?', '&', True, '=', False),
|
|
84
|
+
'&': ('&', '&', True, '=', False),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# The operators that RFC 6570 reserves for its future extensions
|
|
88
|
+
_FUTURE = '=,!@|'
|
|
89
|
+
|
|
90
|
+
# The tokens of a template, the variables of an expression, and the percent-encoded triplets of a value
|
|
91
|
+
_TOKEN = re.compile(r'\{([^{}]*)\}|([^{}]+)|(.)', re.S)
|
|
92
|
+
_VARSPEC = re.compile(
|
|
93
|
+
r'((?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})(?:\.?(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2}))*)(?::([1-9][0-9]{0,3})|(\*))?'
|
|
94
|
+
)
|
|
95
|
+
_TRIPLET = re.compile(r'(%[0-9A-Fa-f]{2})')
|
|
96
|
+
|
|
97
|
+
# The characters that the unreserved expansion keeps, and the ones that the reserved expansion keeps too
|
|
98
|
+
_UNRESERVED = r'A-Za-z0-9\-._~'
|
|
99
|
+
_RESERVED = r":/?#\[\]@!$&'()*+,;="
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class _Expression:
|
|
103
|
+
# An expression of a template, with its operator and its variables, the names with their prefixes and explodes
|
|
104
|
+
__slots__ = ('content', 'explode', 'first', 'ifemp', 'named', 'op', 'reserved', 'sep', 'variables')
|
|
105
|
+
|
|
106
|
+
def __init__(self, op: str, variables: list[tuple[str, int | None, bool]]) -> None:
|
|
107
|
+
self.op = op
|
|
108
|
+
self.first, self.sep, self.named, self.ifemp, self.reserved = _OPERATORS[op]
|
|
109
|
+
self.variables = variables
|
|
110
|
+
self.explode = any(explode for _, _, explode in variables)
|
|
111
|
+
|
|
112
|
+
# The characters of its content, after its first character, which match takes
|
|
113
|
+
extra = ',' + _RESERVED if self.reserved else ','
|
|
114
|
+
if self.named:
|
|
115
|
+
extra += '=' + self.sep
|
|
116
|
+
elif len(variables) > 1 or self.explode:
|
|
117
|
+
extra += self.sep
|
|
118
|
+
self.content = re.compile(rf'(?:[{_UNRESERVED}{re.escape(extra)}]|%[0-9A-Fa-f]{{2}})*')
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def wide(self) -> bool:
|
|
122
|
+
# An expression that takes what the others leave, whose value has the separators
|
|
123
|
+
return self.reserved or self.explode
|
|
124
|
+
|
|
125
|
+
def expand(self, values: Mapping[str, Any]) -> str:
|
|
126
|
+
items = [
|
|
127
|
+
self._varspec(name, prefix, explode, value)
|
|
128
|
+
for name, prefix, explode in self.variables
|
|
129
|
+
if (value := values.get(name)) is not None and not (isinstance(value, list | tuple | dict) and not value)
|
|
130
|
+
]
|
|
131
|
+
return self.first + self.sep.join(items) if items else ''
|
|
132
|
+
|
|
133
|
+
def values(self, content: str | None) -> dict[str, Any] | None:
|
|
134
|
+
# The values of the variables of the content of the expression, or None for a content that it does not expand to
|
|
135
|
+
if content is None:
|
|
136
|
+
return {}
|
|
137
|
+
if not self.content.fullmatch(content):
|
|
138
|
+
return None
|
|
139
|
+
if self.named:
|
|
140
|
+
return self._named(content)
|
|
141
|
+
|
|
142
|
+
# An empty expression without a first character is one without values, as an undefined variable
|
|
143
|
+
if not content and not self.first:
|
|
144
|
+
return {}
|
|
145
|
+
if len(self.variables) == 1:
|
|
146
|
+
name, _, explode = self.variables[0]
|
|
147
|
+
return {name: [_decode(part) for part in content.split(self.sep)] if explode else _decode(content)}
|
|
148
|
+
parts = content.split(self.sep)
|
|
149
|
+
if len(parts) > len(self.variables):
|
|
150
|
+
return None
|
|
151
|
+
return {name: _decode(part) for (name, _, _), part in zip(self.variables, parts, strict=False)}
|
|
152
|
+
|
|
153
|
+
def _escape(self, value: str) -> str:
|
|
154
|
+
return _escape(value, self.reserved)
|
|
155
|
+
|
|
156
|
+
def _named(self, content: str) -> dict[str, Any] | None:
|
|
157
|
+
names = {name: explode for name, _, explode in self.variables}
|
|
158
|
+
values: dict[str, Any] = {}
|
|
159
|
+
exploded: dict[str, str] = {}
|
|
160
|
+
for item in content.split(self.sep) if content else []:
|
|
161
|
+
name, _, value = item.partition('=')
|
|
162
|
+
value = _decode(value)
|
|
163
|
+
if (explode := names.get(name)) is None:
|
|
164
|
+
if not self.explode:
|
|
165
|
+
return None
|
|
166
|
+
exploded[name] = value
|
|
167
|
+
elif explode:
|
|
168
|
+
values.setdefault(name, []).append(value)
|
|
169
|
+
else:
|
|
170
|
+
values[name] = value
|
|
171
|
+
|
|
172
|
+
# The pairs of an exploded variable whose names are not its own, as the keys of a dict
|
|
173
|
+
if exploded:
|
|
174
|
+
name = next(name for name, _, explode in self.variables if explode)
|
|
175
|
+
if name in values:
|
|
176
|
+
return None
|
|
177
|
+
values[name] = exploded
|
|
178
|
+
return values
|
|
179
|
+
|
|
180
|
+
def _varspec(self, name: str, prefix: int | None, explode: bool, value: Any) -> str:
|
|
181
|
+
# A variable of the expression, as the appendix A of RFC 6570 expands it
|
|
182
|
+
if not isinstance(value, list | tuple | dict):
|
|
183
|
+
string = _str(value)
|
|
184
|
+
if prefix is not None:
|
|
185
|
+
string = string[:prefix]
|
|
186
|
+
if self.named:
|
|
187
|
+
return name + ('=' + self._escape(string) if string else self.ifemp)
|
|
188
|
+
return self._escape(string)
|
|
189
|
+
|
|
190
|
+
if prefix is not None:
|
|
191
|
+
raise Pyjo.Exception.new(f'Prefix modifier of a variable with a list or a dict: {name}')
|
|
192
|
+
if isinstance(value, dict):
|
|
193
|
+
pairs = [(_str(key), _str(item)) for key, item in cast(dict[Any, Any], value).items()]
|
|
194
|
+
else:
|
|
195
|
+
pairs = [(name, _str(item)) for item in cast(list[Any], value)]
|
|
196
|
+
|
|
197
|
+
if not explode:
|
|
198
|
+
if isinstance(value, dict):
|
|
199
|
+
joined = ','.join(f'{self._escape(key)},{self._escape(item)}' for key, item in pairs)
|
|
200
|
+
else:
|
|
201
|
+
joined = ','.join(self._escape(item) for _, item in pairs)
|
|
202
|
+
return f'{name}={joined}' if self.named else joined
|
|
203
|
+
|
|
204
|
+
if self.named:
|
|
205
|
+
return self.sep.join(
|
|
206
|
+
self._escape(key) + ('=' + self._escape(item) if item else self.ifemp) for key, item in pairs
|
|
207
|
+
)
|
|
208
|
+
if isinstance(value, dict):
|
|
209
|
+
return self.sep.join(f'{self._escape(key)}={self._escape(item)}' for key, item in pairs)
|
|
210
|
+
return self.sep.join(self._escape(item) for _, item in pairs)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Pyjo_URL_Template(Pyjo.Base.object):
|
|
214
|
+
"""
|
|
215
|
+
:mod:`Pyjo.URL.Template` inherits all methods from :mod:`Pyjo.Base` and
|
|
216
|
+
implements the following new ones.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
def __init__(self, template: str = '') -> None:
|
|
220
|
+
"""::
|
|
221
|
+
|
|
222
|
+
template = Pyjo.URL.Template.new('/search{?q,lang}')
|
|
223
|
+
|
|
224
|
+
Construct a new :mod:`Pyjo.URL.Template` object from a template, or
|
|
225
|
+
raise a :mod:`Pyjo.Exception` for one that is not valid.
|
|
226
|
+
"""
|
|
227
|
+
self.template = template
|
|
228
|
+
"""::
|
|
229
|
+
|
|
230
|
+
template = tpl.template
|
|
231
|
+
|
|
232
|
+
The template, as a string.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
self._tokens: list[str | _Expression] = []
|
|
236
|
+
for expression, literal, other in _TOKEN.findall(template):
|
|
237
|
+
if other:
|
|
238
|
+
raise Pyjo.Exception.new(f'Unmatched brace in URI template: {template}')
|
|
239
|
+
if literal:
|
|
240
|
+
# A literal of the URI as it is, with the characters that a URI does not have encoded
|
|
241
|
+
self._tokens.append(_escape(literal, True))
|
|
242
|
+
continue
|
|
243
|
+
self._tokens.append(_expression(expression, template))
|
|
244
|
+
self._matcher: tuple[int, list[str | _Expression]] | None = None
|
|
245
|
+
|
|
246
|
+
def __str__(self) -> str:
|
|
247
|
+
"""::
|
|
248
|
+
|
|
249
|
+
string = str(tpl)
|
|
250
|
+
|
|
251
|
+
The template.
|
|
252
|
+
"""
|
|
253
|
+
return self.template
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def variables(self) -> list[str]:
|
|
257
|
+
"""::
|
|
258
|
+
|
|
259
|
+
names = tpl.variables
|
|
260
|
+
|
|
261
|
+
The names of the variables of the template, in the order of the
|
|
262
|
+
template, each once. ::
|
|
263
|
+
|
|
264
|
+
# ['owner', 'repo', 'state']
|
|
265
|
+
Pyjo.URL.Template.new('/repos/{owner}/{repo}/issues{?state}').variables
|
|
266
|
+
"""
|
|
267
|
+
names: dict[str, None] = {}
|
|
268
|
+
for token in self._tokens:
|
|
269
|
+
if isinstance(token, _Expression):
|
|
270
|
+
names.update((name, None) for name, _, _ in token.variables)
|
|
271
|
+
return list(names)
|
|
272
|
+
|
|
273
|
+
def expand(self, variables: Mapping[str, Any] | None = None, /, **kwargs: Any) -> Pyjo.String.Unicode.object:
|
|
274
|
+
"""::
|
|
275
|
+
|
|
276
|
+
url = tpl.expand(q='Hello World!')
|
|
277
|
+
url = tpl.expand({'q': 'Hello World!', 'lang': 'en'})
|
|
278
|
+
|
|
279
|
+
Expand the template with the values of its variables, as a
|
|
280
|
+
:class:`dict` or as keyword arguments, and return the URI as a
|
|
281
|
+
:mod:`Pyjo.String.Unicode` object, with the encoding that the RFC
|
|
282
|
+
gives it, which :mod:`Pyjo.URL` would normalize. A value is a string, a number, a list or a
|
|
283
|
+
tuple of them, or a :class:`dict` of them, and ``None``, an empty list
|
|
284
|
+
and an empty dict are undefined, which their expressions leave out. A
|
|
285
|
+
prefix modifier of a list or a dict raises a :mod:`Pyjo.Exception`. ::
|
|
286
|
+
|
|
287
|
+
# "/repos/dex4er/Pyjoyment/issues?labels=bug,docs"
|
|
288
|
+
template = Pyjo.URL.Template.new('/repos/{owner}/{repo}/issues{?labels}')
|
|
289
|
+
template.expand(owner='dex4er', repo='Pyjoyment', labels=['bug', 'docs'])
|
|
290
|
+
"""
|
|
291
|
+
values = {**(variables or {}), **kwargs}
|
|
292
|
+
return Pyjo.String.Unicode.new(
|
|
293
|
+
''.join(token if isinstance(token, str) else token.expand(values) for token in self._tokens)
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def match(self, uri: Anything) -> dict[str, Any] | None:
|
|
297
|
+
"""::
|
|
298
|
+
|
|
299
|
+
variables = tpl.match('/search?q=Hello%20World%21')
|
|
300
|
+
|
|
301
|
+
Match a URI, a string or a :mod:`Pyjo.URL` object, with the template,
|
|
302
|
+
and return the values of the variables that it has, decoded, or
|
|
303
|
+
``None`` for a URI that does not match. The value of a variable with
|
|
304
|
+
the explode modifier is a list, or a :class:`dict` for the pairs of a
|
|
305
|
+
named one, and a list without it is a string with the commas, as the
|
|
306
|
+
template cannot tell it from a string. A template whose expressions
|
|
307
|
+
cannot be told apart, two of them that take what the others leave, or
|
|
308
|
+
two that follow each other without a literal or the first character
|
|
309
|
+
of an operator between them, raises a :mod:`Pyjo.Exception`. ::
|
|
310
|
+
|
|
311
|
+
# {'q': 'Hello World!'}
|
|
312
|
+
Pyjo.URL.Template.new('/search{?q,lang}').match('/search?q=Hello%20World%21')
|
|
313
|
+
|
|
314
|
+
# {'path': ['src', 'main.py']}
|
|
315
|
+
Pyjo.URL.Template.new('file:///project{/path*}').match('file:///project/src/main.py')
|
|
316
|
+
"""
|
|
317
|
+
wide, tokens = self._compile()
|
|
318
|
+
string = str(uri)
|
|
319
|
+
|
|
320
|
+
# The tokens before the expression that takes what the others leave, from the beginning of the URI
|
|
321
|
+
contents: list[tuple[_Expression, str | None]] = []
|
|
322
|
+
start = 0
|
|
323
|
+
for i in range(wide):
|
|
324
|
+
token = tokens[i]
|
|
325
|
+
if isinstance(token, str):
|
|
326
|
+
if not string.startswith(token, start):
|
|
327
|
+
return None
|
|
328
|
+
start += len(token)
|
|
329
|
+
continue
|
|
330
|
+
if token.first:
|
|
331
|
+
if not string.startswith(token.first, start):
|
|
332
|
+
contents.append((token, None))
|
|
333
|
+
continue
|
|
334
|
+
start += 1
|
|
335
|
+
end = string.find(stop, start) if (stop := _first(tokens, i + 1)) else -1
|
|
336
|
+
end = len(string) if end < 0 else end
|
|
337
|
+
contents.append((token, string[start:end]))
|
|
338
|
+
start = end
|
|
339
|
+
|
|
340
|
+
# The tokens after it, from the end of the URI
|
|
341
|
+
end = len(string)
|
|
342
|
+
for i in range(len(tokens) - 1, wide, -1):
|
|
343
|
+
token = tokens[i]
|
|
344
|
+
if isinstance(token, str):
|
|
345
|
+
if end - len(token) < start or not string.startswith(token, end - len(token)):
|
|
346
|
+
return None
|
|
347
|
+
end -= len(token)
|
|
348
|
+
continue
|
|
349
|
+
delimiter = token.first or _last(tokens, i - 1)
|
|
350
|
+
at = string.rfind(delimiter, start, end) if delimiter else -1
|
|
351
|
+
at = start - 1 if at < 0 and not token.first else at
|
|
352
|
+
|
|
353
|
+
# The first character of an operator that separates the variables too, once for each of them
|
|
354
|
+
for _ in range(len(token.variables) - 1 if token.first == token.sep else 0):
|
|
355
|
+
if (before := string.rfind(delimiter, start, at)) < 0:
|
|
356
|
+
break
|
|
357
|
+
at = before
|
|
358
|
+
if token.first:
|
|
359
|
+
# An expression of a first character that is not there, or not its own, is one that is not there
|
|
360
|
+
if at < 0 or token.values(string[at + 1 : end]) is None:
|
|
361
|
+
contents.append((token, None))
|
|
362
|
+
continue
|
|
363
|
+
contents.append((token, string[at + 1 : end]))
|
|
364
|
+
end = at
|
|
365
|
+
continue
|
|
366
|
+
contents.append((token, string[at + 1 : end]))
|
|
367
|
+
end = at + 1
|
|
368
|
+
|
|
369
|
+
# The expression in between, of what is left
|
|
370
|
+
if wide < len(tokens):
|
|
371
|
+
token = cast(_Expression, tokens[wide])
|
|
372
|
+
middle = string[start:end]
|
|
373
|
+
if token.first:
|
|
374
|
+
if middle and not middle.startswith(token.first):
|
|
375
|
+
return None
|
|
376
|
+
contents.append((token, middle[1:] if middle else None))
|
|
377
|
+
else:
|
|
378
|
+
contents.append((token, middle))
|
|
379
|
+
elif start != end:
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
values: dict[str, Any] = {}
|
|
383
|
+
for token, content in contents:
|
|
384
|
+
if (found := token.values(content)) is None:
|
|
385
|
+
return None
|
|
386
|
+
values.update(found)
|
|
387
|
+
return values
|
|
388
|
+
|
|
389
|
+
def _compile(self) -> tuple[int, list[str | _Expression]]:
|
|
390
|
+
# The tokens of the template, and the index of the expression that takes what the others leave, once
|
|
391
|
+
if (matcher := self._matcher) is not None:
|
|
392
|
+
return matcher
|
|
393
|
+
tokens = self._tokens
|
|
394
|
+
expressions = [i for i, token in enumerate(tokens) if isinstance(token, _Expression)]
|
|
395
|
+
wides = [i for i in expressions if cast(_Expression, tokens[i]).wide]
|
|
396
|
+
if len(wides) > 1:
|
|
397
|
+
raise Pyjo.Exception.new(f'More than one expression of a reserved or exploded value: {self.template}')
|
|
398
|
+
for i in expressions:
|
|
399
|
+
if i + 1 < len(tokens) and isinstance(after := tokens[i + 1], _Expression) and not after.first:
|
|
400
|
+
raise Pyjo.Exception.new(f'Expressions without a literal between them: {self.template}')
|
|
401
|
+
wide = wides[0] if wides else expressions[-1] if expressions else len(tokens)
|
|
402
|
+
self._matcher = (wide, tokens)
|
|
403
|
+
return self._matcher
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _decode(value: str) -> str:
|
|
407
|
+
# A percent-encoded value of UTF-8, or of its bytes as characters where they are not UTF-8, as Pyjo.URL decodes
|
|
408
|
+
data = url_unescape(value.encode())
|
|
409
|
+
decoded = decode('UTF-8', data)
|
|
410
|
+
return decoded if decoded is not None else data.decode('latin-1')
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _escape(value: str, reserved: bool) -> str:
|
|
414
|
+
# A value percent-encoded as UTF-8, with the reserved characters and the percent-encoded triplets of a reserved one
|
|
415
|
+
if not reserved:
|
|
416
|
+
return url_escape(value.encode(), f'^{_UNRESERVED}'.encode()).decode()
|
|
417
|
+
return ''.join(
|
|
418
|
+
part if _TRIPLET.fullmatch(part) else url_escape(part.encode(), f'^{_UNRESERVED}{_RESERVED}'.encode()).decode()
|
|
419
|
+
for part in _TRIPLET.split(value)
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _expression(expression: str, template: str) -> _Expression:
|
|
424
|
+
# An expression of a template, its operator and the list of its variables
|
|
425
|
+
op = expression[:1] if expression[:1] in _OPERATORS else ''
|
|
426
|
+
if not expression or expression[:1] in _FUTURE:
|
|
427
|
+
raise Pyjo.Exception.new(f'Invalid expression in URI template: {{{expression}}} of {template}')
|
|
428
|
+
variables: list[tuple[str, int | None, bool]] = []
|
|
429
|
+
for varspec in expression[len(op) :].split(','):
|
|
430
|
+
if (spec := _VARSPEC.fullmatch(varspec)) is None:
|
|
431
|
+
raise Pyjo.Exception.new(f'Invalid expression in URI template: {{{expression}}} of {template}')
|
|
432
|
+
variables.append((spec[1], int(spec[2]) if spec[2] else None, bool(spec[3])))
|
|
433
|
+
return _Expression(op, variables)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _first(tokens: list[str | _Expression], i: int) -> str:
|
|
437
|
+
# The first character of a token, which ends the expression before it, or nothing at the end of the template
|
|
438
|
+
if i >= len(tokens):
|
|
439
|
+
return ''
|
|
440
|
+
token = tokens[i]
|
|
441
|
+
return token[0] if isinstance(token, str) else token.first
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _last(tokens: list[str | _Expression], i: int) -> str:
|
|
445
|
+
# The last character of the literal before an expression without a first character
|
|
446
|
+
token = tokens[i]
|
|
447
|
+
return token[-1] if isinstance(token, str) else ''
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _str(value: Any) -> str:
|
|
451
|
+
# A value as a string, a boolean as in JSON
|
|
452
|
+
if isinstance(value, bool):
|
|
453
|
+
return 'true' if value else 'false'
|
|
454
|
+
return str(value)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
new = Pyjo_URL_Template.new
|
|
458
|
+
object = Pyjo_URL_Template
|
|
459
|
+
Template = Pyjo_URL_Template
|