WhakerPy 2.2__py3-none-any.whl → 2.3__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.
- whakerpy/__init__.py +1 -1
- whakerpy/htmlmaker/htmnodes/htmnode.py +1 -1
- whakerpy/httpd/handler.py +8 -0
- whakerpy/httpd/hutils.py +7 -1
- whakerpy/httpd/permissions.py +9 -8
- whakerpy/webapp/webconfig.py +3 -3
- {whakerpy-2.2.dist-info → whakerpy-2.3.dist-info}/METADATA +70 -39
- {whakerpy-2.2.dist-info → whakerpy-2.3.dist-info}/RECORD +12 -12
- {whakerpy-2.2.dist-info → whakerpy-2.3.dist-info}/WHEEL +0 -0
- {whakerpy-2.2.dist-info → whakerpy-2.3.dist-info}/licenses/AUTHORS.md +0 -0
- {whakerpy-2.2.dist-info → whakerpy-2.3.dist-info}/licenses/LICENSE.md +0 -0
- {whakerpy-2.2.dist-info → whakerpy-2.3.dist-info}/top_level.txt +0 -0
whakerpy/__init__.py
CHANGED
|
@@ -25,6 +25,6 @@ from .httpd import *
|
|
|
25
25
|
from .messages import error
|
|
26
26
|
from .webapp import *
|
|
27
27
|
|
|
28
|
-
__version__ = "2.
|
|
28
|
+
__version__ = "2.3"
|
|
29
29
|
___author__ = "Brigitte Bigi"
|
|
30
30
|
__copyright__ = "Copyright (C) 2023-2026 Brigitte Bigi, CNRS, Laboratoire Parole et Langage, Aix-en-Provence, France"
|
|
@@ -284,7 +284,7 @@ class TagNode(BaseTagNode):
|
|
|
284
284
|
html = ""
|
|
285
285
|
try:
|
|
286
286
|
# For some tags, the space char is meaningful. textarea is one of them. others???
|
|
287
|
-
if self.tag
|
|
287
|
+
if self.tag not in ("textarea", "pre"):
|
|
288
288
|
html += indent + " " * nbs
|
|
289
289
|
html += self._value
|
|
290
290
|
html += "\n"
|
whakerpy/httpd/handler.py
CHANGED
|
@@ -218,6 +218,14 @@ class HTTPDHandler(http.server.BaseHTTPRequestHandler):
|
|
|
218
218
|
elif mime_type == "text/html":
|
|
219
219
|
events = HTTPDHandlerUtils.parse_query_string(query_string)
|
|
220
220
|
content, status = self._bakery(handler_utils, events, mime_type)
|
|
221
|
+
# A GET is a navigation, not a form submission: query
|
|
222
|
+
# parameters the recipe does not handle (status 205) must
|
|
223
|
+
# not prevent the page from being served. The baked content
|
|
224
|
+
# is complete: serve it with a 200.
|
|
225
|
+
if status.code == 205:
|
|
226
|
+
logging.warning("Ignored unknown GET events {} for page {}"
|
|
227
|
+
"".format(events, self.path))
|
|
228
|
+
status = HTTPDStatus(200)
|
|
221
229
|
|
|
222
230
|
# Unknown mime type: try to get a static file anyway.
|
|
223
231
|
else:
|
whakerpy/httpd/hutils.py
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
..
|
|
11
11
|
-------------------------------------------------------------------------
|
|
12
12
|
|
|
13
|
-
Copyright (C) 2023-
|
|
13
|
+
Copyright (C) 2023-2026 Brigitte Bigi, CNRS
|
|
14
14
|
Laboratoire Parole et Langage, Aix-en-Provence, France
|
|
15
15
|
|
|
16
16
|
This program is free software: you can redistribute it and/or modify
|
|
@@ -49,6 +49,12 @@ from .hstatus import HTTPDStatus
|
|
|
49
49
|
from .permissions import UnixPermissions
|
|
50
50
|
from .permissions import FileAccessChecker
|
|
51
51
|
|
|
52
|
+
# The stdlib "mimetypes" default type for ".js" changed across Python
|
|
53
|
+
# versions (RFC 9239 moved it from "application/javascript" to
|
|
54
|
+
# "text/javascript"). Pin it explicitly so the mime type returned by
|
|
55
|
+
# get_mime_type() is identical on every supported Python version.
|
|
56
|
+
mimetypes.add_type("text/javascript", ".js")
|
|
57
|
+
|
|
52
58
|
# -----------------------------------------------------------------------
|
|
53
59
|
|
|
54
60
|
|
whakerpy/httpd/permissions.py
CHANGED
|
@@ -203,21 +203,22 @@ class FileAccessChecker:
|
|
|
203
203
|
def __check_permission_for_role(self, role: str) -> bool:
|
|
204
204
|
"""Helper function to check permissions for a single role.
|
|
205
205
|
|
|
206
|
+
The permission of a role is given by the mode bits of the file
|
|
207
|
+
only: the role does not have to match the effective uid/gid of
|
|
208
|
+
the current process. It answers "is the file readable by this
|
|
209
|
+
role", not "can the current process read it as this role" --
|
|
210
|
+
otherwise no file can be served from a mount owning the files
|
|
211
|
+
to a foreign user, like a FUSE/NTFS partition.
|
|
212
|
+
|
|
206
213
|
:param role: (str) Who to check permissions for: 'others', 'group', or 'owner'.
|
|
207
214
|
|
|
208
215
|
"""
|
|
209
|
-
current_uid = os.geteuid() # Effective user ID of the current process
|
|
210
|
-
current_gid = os.getegid() # Effective group ID of the current process
|
|
211
|
-
|
|
212
|
-
# Check owner, group, and others' permissions
|
|
213
216
|
mode = self.__file_stat.st_mode
|
|
214
|
-
owner_uid = self.__file_stat.st_uid
|
|
215
|
-
group_gid = self.__file_stat.st_gid
|
|
216
217
|
|
|
217
218
|
# Determine read permission based on the role
|
|
218
|
-
if role == "owner"
|
|
219
|
+
if role == "owner":
|
|
219
220
|
return bool(mode & stat.S_IRUSR) # Owner read permission
|
|
220
|
-
elif role == "group"
|
|
221
|
+
elif role == "group":
|
|
221
222
|
return bool(mode & stat.S_IRGRP) # Group read permission
|
|
222
223
|
elif role == "others":
|
|
223
224
|
return bool(mode & stat.S_IROTH) # Others' read permission
|
whakerpy/webapp/webconfig.py
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
..
|
|
9
9
|
-------------------------------------------------------------------------
|
|
10
10
|
|
|
11
|
-
Copyright (C) 2023-
|
|
11
|
+
Copyright (C) 2023-2026 Brigitte Bigi
|
|
12
12
|
Laboratoire Parole et Langage, Aix-en-Provence, France
|
|
13
13
|
|
|
14
14
|
This program is free software: you can redistribute it and/or modify
|
|
@@ -270,8 +270,8 @@ class WebSiteData:
|
|
|
270
270
|
def __get_json_whakerpy_section(filename: str):
|
|
271
271
|
"""Return the configuration section related to WhakerPy.
|
|
272
272
|
|
|
273
|
-
|
|
274
|
-
|
|
273
|
+
Requires a top-level "WhakerPy" key, introduced in WhakerPy 1.2.
|
|
274
|
+
Configuration files predating this key are not supported.
|
|
275
275
|
|
|
276
276
|
:param filename: path to JSON configuration file
|
|
277
277
|
:return: dict with keys "pagespath", "<page>.html", …
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: WhakerPy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3
|
|
4
4
|
Summary: Web HTML maker in Python - a light web application framework
|
|
5
5
|
Author-email: Brigitte Bigi <contact@sppas.org>
|
|
6
6
|
Project-URL: Repo, https://github.com/brigitte-bigi/WhakerPy
|
|
@@ -53,47 +53,86 @@ Dynamic: license-file
|
|
|
53
53
|
|
|
54
54
|
### Uses cases
|
|
55
55
|
|
|
56
|
-
You want
|
|
57
|
-
WhakerPy is a Python library of such a tool.
|
|
56
|
+
You want your users to reach dynamic web pages from a browser, without adopting a full templating stack. WhakerPy is a Python library to build and serve that content.
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
The WhakerPy library does not offer views, templates or models!
|
|
61
|
-
On the contrary, **WhakerPy offers a 100% pure-Python solution** by creating HTML pages entirely dynamically, or with a static content.
|
|
58
|
+
Django and Flask both render a template file against a model. WhakerPy has no views, no templates and no models: a page is an HTML tree, built and serialized directly in Python.
|
|
62
59
|
|
|
63
60
|
WhakerPy is your solution if:
|
|
64
|
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
61
|
+
|
|
62
|
+
- you want a relatively simple web app with a few static or semi-dynamic pages;
|
|
63
|
+
- you want full, programmatic control over the generated HTML, without a template language to learn;
|
|
64
|
+
- **you want to build and secure a web app with nothing but Python and its standard library**.
|
|
67
65
|
|
|
68
66
|
|
|
69
67
|
### Features
|
|
70
68
|
|
|
71
|
-
WhakerPy is a
|
|
72
|
-
WhakerPy is then a collection of packages and modules that help developers to create static or dynamic web content without having to worry about the details involved.
|
|
69
|
+
WhakerPy is a free, open-source, self-hosted Python library — not a framework — to build dynamic HTML content and web applications. It stays out of the way: no views, no templates, no models, no third-party dependency for the core library.
|
|
73
70
|
|
|
74
|
-
* Build
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
|
|
71
|
+
* Build an HTML page as a tree of Python objects (`HTMLTree`, `HTMLNode`), then serialize it to a string or to a static file
|
|
72
|
+
* Serve pages dynamically with a "bakery" response system that re-renders only what changed
|
|
73
|
+
* Deploy the same application three ways, unchanged: as static files, behind the built-in HTTPD dev server, or behind a production WSGI server (Apache, nginx, gunicorn, ...)
|
|
74
|
+
* Parse GET query strings, POST bodies (JSON, url-encoded forms) and multipart file uploads yourself — hand-written against the standard library, no `python-multipart`, and no dependency on the `cgi` module removed from Python 3.13
|
|
75
|
+
* Protect pages and endpoints with a built-in HTTP security policy, shared by the HTTPD server and the WSGI application:
|
|
76
|
+
- a configurable blacklist rejecting requests by URL path or User-Agent
|
|
77
|
+
- HMAC-SHA256 signed, time-limited URLs (tokens) to share ephemeral links without sessions, cookies or server-side storage — no `itsdangerous`, no `PyJWT`
|
|
78
|
+
* Serve static files only after checking Unix read permissions (owner/group/others), so a misconfigured file is never served by accident
|
|
80
79
|
|
|
81
80
|
|
|
82
81
|
### Main advantages
|
|
83
82
|
|
|
84
|
-
>
|
|
83
|
+
>Build, secure and serve dynamic HTML web applications with nothing but the power of Python!
|
|
84
|
+
|
|
85
|
+
* easy to learn: one consistent, explicit syntax, from the HTML tag tree to the HTTP response
|
|
86
|
+
* a single dependency-free package: HTML generation, HTTPD/WSGI serving, security policy and upload parsing all ship in the Python Standard Library only
|
|
87
|
+
* open-source and easily customizable: pure-Python, Object-Oriented, no compiled extension
|
|
88
|
+
* portable: runs anywhere Python runs, from a local script to a production WSGI deployment
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
## How it works
|
|
92
|
+
|
|
93
|
+
WhakerPy is not Flask, not Django, and does not try to be. There is no template engine and nothing to render:
|
|
94
|
+
a page **is** a tree of Python objects.
|
|
95
|
+
|
|
96
|
+
### No templates — an HTML tree, in Python
|
|
97
|
+
|
|
98
|
+
Flask and Django keep a page as a text file (Jinja2 / Django templates) with placeholders (`{% if %}`, `{% for %}`, `{{ variable }}`) filled from a context dictionary at render time.
|
|
99
|
+
That file is not Python: it has its own syntax, its own scoping rules, and its own way of drifting out of sync with the code that feeds it.
|
|
100
|
+
|
|
101
|
+
WhakerPy has no such file. An `HTMLTree` is a tree of `HTMLNode` objects — `<head>`, `<body>` and every element inside are Python instances, built and traversed with ordinary Python: `if`, `for`, functions, classes.
|
|
102
|
+
To change what gets rendered, write Python; there is no second language to learn:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
>>> if user.is_admin:
|
|
106
|
+
... htree.body_main.append_child(HTMLNode(main_id, None, "p", value="Admin panel"))
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
That is the whole of "the WhakerPy templating language".
|
|
85
110
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
111
|
+
### One page class, three ways to serve it
|
|
112
|
+
|
|
113
|
+
A page is a subclass of `BaseResponseRecipe`. `create()` builds the parts of the tree that never change; `bake(events)` runs on every request, decides from the incoming `events` (query string or POST data) whether anything changed, and only rebuilds the dynamic part when it did — a static page is never re-serialized for nothing.
|
|
114
|
+
|
|
115
|
+
The same recipe instances can then be exposed three ways, without touching the page code:
|
|
116
|
+
|
|
117
|
+
- `tree.serialize_to_file(...)` — dumped once to a static `.html` file;
|
|
118
|
+
- `BaseHTTPDServer` / `HTTPDHandler` — the stdlib `http.server`, for local development or a small deployment with zero extra process;
|
|
119
|
+
- `WSGIApplication` — the same recipes, the same bakery, behind any production WSGI server (Apache, nginx + gunicorn/uWSGI, ...).
|
|
120
|
+
|
|
121
|
+
### Security is a policy, not a decorator
|
|
122
|
+
|
|
123
|
+
`HTTPDPolicy` sits in front of both servers and is configured once: a blacklist of paths/User-Agents, and a set of HMAC-signed, time-limited URLs.
|
|
124
|
+
It applies identically whether a request comes through the dev HTTPD server or through WSGI in production — one place to configure it, not one per route.
|
|
125
|
+
|
|
126
|
+
### Minimalism as a security stance
|
|
127
|
+
|
|
128
|
+
WhakerPy is deliberately small: code that is not written cannot be exploited. The handlers only emit the status codes their own logic actually needs — 200 (OK), 205 (Reset Content, an unhandled GET is still served), 403 (Forbidden), 404 (Not Found, including an expired signed URL), 410 (Gone, shuts the server down), 418 (I'm a Teapot, a blacklisted request), 500 (Internal Server Error) — which covers the large majority of real needs, instead of a generic status-code framework bolted onto every route.
|
|
129
|
+
|
|
130
|
+
`HTTPDStatus` still validates against the full IANA status registry, so any `BaseResponseRecipe` is free to return whichever code its page actually needs. If the built-in handlers should react to more of them, that is what contributions are for.
|
|
92
131
|
|
|
93
132
|
|
|
94
133
|
## Get and install WhakerPy
|
|
95
134
|
|
|
96
|
-
Get it from its repository <https://
|
|
135
|
+
Get it from its repository <https://github.com/brigitte-bigi/WhakerPy> or from Pypi <https://pypi.org/project/whakerpy/>, and get documentation <https://whakerpy.sourceforge.io>.
|
|
97
136
|
|
|
98
137
|
### Install from pypi.org:
|
|
99
138
|
|
|
@@ -119,8 +158,8 @@ Download the latest ".zip" from it's web page and unpack it, or clone the reposi
|
|
|
119
158
|
4. "sample": a web application sample
|
|
120
159
|
|
|
121
160
|
```bash
|
|
122
|
-
> unzip WhakerPy-
|
|
123
|
-
> git clone https://
|
|
161
|
+
> unzip WhakerPy-2.2.zip
|
|
162
|
+
> git clone https://github.com/brigitte-bigi/WhakerPy.git whakerpy-code
|
|
124
163
|
> python -m pip install .
|
|
125
164
|
```
|
|
126
165
|
|
|
@@ -197,23 +236,15 @@ created from a custom `BaseResponseRecipe()` object, available in the file
|
|
|
197
236
|
`sample/response.py`. The response is the interface between a local back-end
|
|
198
237
|
python application and the web front-end.
|
|
199
238
|
|
|
200
|
-
For a more complex example of an already in-used application web frontend, see:
|
|
201
|
-
<https://sourceforge.net/p/sppas/code/ci/master/tree/sppas/ui/swapp/app_setup/setupmaker.py>.
|
|
202
|
-
|
|
203
239
|
|
|
204
240
|
## Projects using WhakerPy
|
|
205
241
|
|
|
206
|
-
WhakerPy was initially developed within SPPAS <https://sppas.org>.
|
|
207
|
-
It was extracted from its original software by the author to lead its own life as standalone package.
|
|
208
|
-
The "setup" of SPPAS is entirely based on `whakerpy` API.
|
|
209
|
-
|
|
210
|
-
Other projects:
|
|
211
|
-
|
|
212
242
|
- the website <https://auto-cuedspeech.org>
|
|
213
243
|
- the website <https://sppas.org>
|
|
214
|
-
- the
|
|
244
|
+
- the SPPAS graphical user interface
|
|
215
245
|
- the intranet of the Laboratoire Parole et Langage
|
|
216
|
-
- *contact the author if your project is based on WhakerPy*
|
|
246
|
+
- *contact the author if your project is based on WhakerPy* and you want it to be included in this list.
|
|
247
|
+
|
|
217
248
|
|
|
218
249
|
|
|
219
250
|
# The developer's corner
|
|
@@ -313,4 +344,4 @@ or products, in accordance with the best practices of the AGPL license.
|
|
|
313
344
|
Use the following reference to cite WhakerPy:
|
|
314
345
|
|
|
315
346
|
> Brigitte Bigi. WhakerPy, a Python library to create dynamic HTML content and
|
|
316
|
-
> web applications. Version
|
|
347
|
+
> web applications. Version 2.2. 2026. <https://hal.science/hal-04743687>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
sample/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
sample/response.py,sha256=_jAaWLrHuGBYy0Y0aywxGrSDX365MSfASqWTISIotqw,12532
|
|
3
|
-
whakerpy/__init__.py,sha256=
|
|
3
|
+
whakerpy/__init__.py,sha256=8a4sscqVbpy_jbNYNlShSUTddoNhawGZMESERCy7AO0,1827
|
|
4
4
|
whakerpy/messages.py,sha256=NSnQYFfarU16T59Zryuy2g2x7UwA8xgY_UDwRLQqPm4,2385
|
|
5
5
|
whakerpy/htmlmaker/__init__.py,sha256=zCHYhW37cyK68u90w2tFBLJx59ApGvqgNkxiIzuVtTM,3333
|
|
6
6
|
whakerpy/htmlmaker/hconsts.py,sha256=6I0Mmp72ZBzePGw-nH4qjD7TvENxXUPAT-qTeljwGe8,12115
|
|
@@ -16,25 +16,25 @@ whakerpy/htmlmaker/emptynodes/emptyelts.py,sha256=wqCtFMZU_bTCP4Z63Q3Pgwo1pNC1tM
|
|
|
16
16
|
whakerpy/htmlmaker/emptynodes/emptynode.py,sha256=caUy9V7CG-G_lgVwBAjMNKEdVeI8FBkTT28aREuuyXA,9920
|
|
17
17
|
whakerpy/htmlmaker/htmnodes/__init__.py,sha256=hOrs9nhL2aMCvukuPoOxRdinPAK_QFpWBpmTnp-iTQ8,1547
|
|
18
18
|
whakerpy/htmlmaker/htmnodes/htmelts.py,sha256=CTLX6hQWpulJuwW5F75gLTbKiiERkAkRLZ0LiHz9_lE,5398
|
|
19
|
-
whakerpy/htmlmaker/htmnodes/htmnode.py,sha256=
|
|
19
|
+
whakerpy/htmlmaker/htmnodes/htmnode.py,sha256=nd6T_eWMxsXZxT8OQGKmcA4PNcA5jpE9cOkVJkntS98,11616
|
|
20
20
|
whakerpy/httpd/__init__.py,sha256=A4_Y6oN-8PY3TmGssX6DGKlUsE0wAGsNc5N1oo8HDK8,2031
|
|
21
|
-
whakerpy/httpd/handler.py,sha256=
|
|
21
|
+
whakerpy/httpd/handler.py,sha256=7hZc2rMALqRK2qKI1hYeN6FrrhBNDVcil5hjNdCpO4I,11518
|
|
22
22
|
whakerpy/httpd/hblacklist.py,sha256=H9-hhLGx9StYGo7ThBs23dVuqNRTveGvkt_SftH_oIc,6605
|
|
23
23
|
whakerpy/httpd/hpolicy.py,sha256=LZBY3eSz9xrpPUlV-ymQxdx34LN33acX9SBsqaNVrm0,9827
|
|
24
24
|
whakerpy/httpd/hresponse.py,sha256=bjv-PS0xQ3RR7z9XvzOptxWkSdmlutRuFWqgdUqmDVI,9412
|
|
25
25
|
whakerpy/httpd/hserver.py,sha256=1X8lGKIZNBFyoNHcLHtQPkOkEORvZ19HLGahOeE8uiY,6302
|
|
26
26
|
whakerpy/httpd/hsignedurl.py,sha256=ZLGENB3-Y1jOANaMP0E71s3xqFmdbBb8hgJf7IODG1g,13295
|
|
27
27
|
whakerpy/httpd/hstatus.py,sha256=rrWDvPXTyAh8l4IJY0xBDkZxeKC7t3_5-XlVHtUDSo8,7998
|
|
28
|
-
whakerpy/httpd/hutils.py,sha256=
|
|
29
|
-
whakerpy/httpd/permissions.py,sha256=
|
|
28
|
+
whakerpy/httpd/hutils.py,sha256=OaEpCm54PfY8jNQePszKFUoSh8Kr_3iUkAWIEZ687hc,28951
|
|
29
|
+
whakerpy/httpd/permissions.py,sha256=KUpLR_qB_mjCzrg3xcCIgErSxD9OJQl72HTaqiOJeyw,8511
|
|
30
30
|
whakerpy/webapp/__init__.py,sha256=FCeq1HYEqFhGyquWajWVhWJbAsOYihBedtdUwpnQvis,276
|
|
31
|
-
whakerpy/webapp/webconfig.py,sha256=
|
|
31
|
+
whakerpy/webapp/webconfig.py,sha256=LmGUrfZ3btFSY3DoMpme2UXgiyHl48VlpnSXUyMk3MM,11215
|
|
32
32
|
whakerpy/webapp/weblocalserver.py,sha256=XjtVkUQPeFsVvX4ARLc_2_9eqc2KBmuQHMlxbr7DMUE,3180
|
|
33
33
|
whakerpy/webapp/webresponse.py,sha256=q4p12QHRFmq52K0lLkdqPJfexkG2SqZLluYF7Zg406g,4430
|
|
34
34
|
whakerpy/webapp/webwsgi.py,sha256=wmdN5MemNqFqrOhQVlpvp0-XSCGGdQ3SF8H4_qJMNG4,11554
|
|
35
|
-
whakerpy-2.
|
|
36
|
-
whakerpy-2.
|
|
37
|
-
whakerpy-2.
|
|
38
|
-
whakerpy-2.
|
|
39
|
-
whakerpy-2.
|
|
40
|
-
whakerpy-2.
|
|
35
|
+
whakerpy-2.3.dist-info/licenses/AUTHORS.md,sha256=VrCusdQ56bvNDiq7w_MA5mZoZXuP_JOG-ybTwpRT_u4,805
|
|
36
|
+
whakerpy-2.3.dist-info/licenses/LICENSE.md,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
|
37
|
+
whakerpy-2.3.dist-info/METADATA,sha256=_x4HiWYvBTUrjLUvS7tOchJ9hrFCRHJbmNgqagdlTtQ,15095
|
|
38
|
+
whakerpy-2.3.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
39
|
+
whakerpy-2.3.dist-info/top_level.txt,sha256=ZqGTnE-rI2S_jcI0kxnNeKd0z25TBv8Vkbe2iJ74-E0,16
|
|
40
|
+
whakerpy-2.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|