pyweber 1.2.0.dev20260428__tar.gz → 1.2.0.dev20260430__tar.gz

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.
Files changed (88) hide show
  1. {pyweber-1.2.0.dev20260428/pyweber.egg-info → pyweber-1.2.0.dev20260430}/PKG-INFO +1 -1
  2. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyproject.toml +1 -1
  3. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/core/element.py +18 -15
  4. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/core/template.py +1 -18
  5. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/element.py +3 -4
  6. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/template_diff.py +19 -24
  7. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430/pyweber.egg-info}/PKG-INFO +1 -1
  8. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/LICENSE +0 -0
  9. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/MANIFEST.in +0 -0
  10. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/README.md +0 -0
  11. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/__init__.py +0 -0
  12. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/admin/index.html +0 -0
  13. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/admin/src/script.js +0 -0
  14. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/admin/src/style.css +0 -0
  15. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/cli/__init__.py +0 -0
  16. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/cli/commands.py +0 -0
  17. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/components/__init__.py +0 -0
  18. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/components/form.py +0 -0
  19. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/components/general.py +0 -0
  20. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/components/input.py +0 -0
  21. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/config/__init__.py +0 -0
  22. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/config/config.py +0 -0
  23. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/connection/__init__.py +0 -0
  24. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/connection/http.py +0 -0
  25. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/connection/reload.py +0 -0
  26. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/connection/selector.py +0 -0
  27. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/connection/session.py +0 -0
  28. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/connection/websocket.py +0 -0
  29. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/core/__init__.py +0 -0
  30. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/core/events.py +0 -0
  31. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/core/window.py +0 -0
  32. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/__init__.py +0 -0
  33. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/cookies.py +0 -0
  34. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/create_app.py +0 -0
  35. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/error_pages.py +0 -0
  36. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/field.py +0 -0
  37. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/field_storage.py +0 -0
  38. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/file.py +0 -0
  39. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/file_stream.py +0 -0
  40. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/headers.py +0 -0
  41. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/middleware.py +0 -0
  42. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/openapi.py +0 -0
  43. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/request.py +0 -0
  44. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/response.py +0 -0
  45. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/routes.py +0 -0
  46. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/run.py +0 -0
  47. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/strem_stats.py +0 -0
  48. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/task_manager.py +0 -0
  49. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/models/ws_message.py +0 -0
  50. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/pyweber/__init__.py +0 -0
  51. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/pyweber/pyweber.py +0 -0
  52. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/.gitignore +0 -0
  53. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/config.toml +0 -0
  54. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/css.css +0 -0
  55. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/docs.html +0 -0
  56. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/favicon/android-chrome-192x192.png +0 -0
  57. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/favicon/android-chrome-512x512.png +0 -0
  58. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/favicon/apple-touch-icon.png +0 -0
  59. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/favicon/favicon-16x16.png +0 -0
  60. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/favicon/favicon-32x32.png +0 -0
  61. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/favicon/favicon.ico +0 -0
  62. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/favicon/pyweber.png +0 -0
  63. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/favicon/site.webmanifest +0 -0
  64. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/handlers.js +0 -0
  65. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/html.html +0 -0
  66. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/html401.html +0 -0
  67. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/html404.html +0 -0
  68. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/html500.html +0 -0
  69. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/js.js +0 -0
  70. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/loading.html +0 -0
  71. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/main.py +0 -0
  72. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/pyweber.css +0 -0
  73. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/static/update.py +0 -0
  74. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/utils/__init__.py +0 -0
  75. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/utils/exceptions.py +0 -0
  76. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/utils/loads.py +0 -0
  77. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/utils/types.py +0 -0
  78. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber/utils/utils.py +0 -0
  79. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber.egg-info/SOURCES.txt +0 -0
  80. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber.egg-info/dependency_links.txt +0 -0
  81. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber.egg-info/entry_points.txt +0 -0
  82. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber.egg-info/requires.txt +0 -0
  83. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/pyweber.egg-info/top_level.txt +0 -0
  84. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/setup.cfg +0 -0
  85. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/tests/test_config.py +0 -0
  86. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/tests/test_cookies.py +0 -0
  87. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/tests/test_response.py +0 -0
  88. {pyweber-1.2.0.dev20260428 → pyweber-1.2.0.dev20260430}/tests/test_template_diff.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyweber
3
- Version: 1.2.0.dev20260428
3
+ Version: 1.2.0.dev20260430
4
4
  Summary: A lightweight Python framework for building and managing web applications.
5
5
  Author-email: DevPythonMZ <pypi.dev@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyweber"
7
- version = "1.2.0.dev20260428"
7
+ version = "1.2.0.dev20260430"
8
8
  description = "A lightweight Python framework for building and managing web applications."
9
9
  readme = "README.md"
10
10
  authors = [{ name = "DevPythonMZ", email = "pypi.dev@gmail.com" }]
@@ -1,8 +1,10 @@
1
+ import os
1
2
  import re
2
3
  from uuid import uuid4
3
4
  import lxml.html as HTMLPARSER
4
5
  from lxml.html import fromstring
5
6
  from typing import Union, Any, Literal
7
+ from pyweber.utils.loads import LoadStaticFiles
6
8
  from pyweber.utils.types import HTMLTag, GetBy
7
9
  from pyweber.models.file import File
8
10
  from pyweber.models.element import (
@@ -244,33 +246,23 @@ class Element(ElementConstrutor): # pragma: no cover
244
246
 
245
247
  def querySelector(self, selector: str, element: 'Element' = None, search_mode: SEARCH_MODE = 'exact') -> 'Element':
246
248
  results = self.querySelectorAll(selector=selector, element=element, search_mode=search_mode)
247
-
248
249
  return results[0] if results else None
249
250
 
250
251
  def querySelectorAll(self, selector: str, element: 'Element' = None, search_mode: SEARCH_MODE = 'exact') -> list['Element']:
251
252
  element = element or self
252
- results: list['Element'] = []
253
253
 
254
254
  if selector.startswith('.'):
255
255
  classes = ' '.join(selector.split('.')).strip()
256
- return self.getElements(by=GetBy.classes, value=classes, search_mode=search_mode)
256
+ return self.getElements(by=GetBy.classes, value=classes, element=element, search_mode=search_mode)
257
257
 
258
258
  elif selector.startswith('#'):
259
- if selector[1:].strip() == element.id:
260
- results.append(element)
259
+ return self.getElements(by='id', value=selector[1:].strip(), element=element, search_mode=search_mode)
261
260
 
262
261
  elif selector.startswith('['):
263
262
  sel = selector.removeprefix('[').removesuffix(']')
264
- return self.getElements(by=GetBy.attrs, value=sel, search_mode=search_mode)
265
-
266
- else:
267
- if selector.strip() == element.tag:
268
- results.append(element)
269
-
270
- for child in element.childs:
271
- results.extend(self.querySelectorAll(selector=selector, element=child, search_mode=search_mode))
263
+ return self.getElements(by='attrs', value=sel, element=element, search_mode=search_mode)
272
264
 
273
- return results
265
+ return self.getElements(by='tag', value=selector.strip(), element=element, search_mode=search_mode)
274
266
 
275
267
  @property
276
268
  def clone(self):
@@ -339,7 +331,7 @@ class Element(ElementConstrutor): # pragma: no cover
339
331
 
340
332
  @classmethod
341
333
  def from_html(cls, html: str, include_uuid: bool = True, **kwargs):
342
- HtmlElement = fromstring(html.strip())
334
+ HtmlElement = fromstring(cls.read_file(html).strip())
343
335
  return cls._create_element(HTMLElement=HtmlElement, parent=None, include_uuid=include_uuid, **kwargs)
344
336
 
345
337
  @classmethod
@@ -422,6 +414,16 @@ class Element(ElementConstrutor): # pragma: no cover
422
414
  except: pass
423
415
  return element
424
416
 
417
+ @classmethod
418
+ def read_file(cls, file_path: str) -> str:
419
+ if file_path.endswith('.html'):
420
+ path = os.path.join('templates', file_path) if not os.path.isfile(file_path) else file_path
421
+
422
+ try: return LoadStaticFiles(path=path).load
423
+ except FileNotFoundError: raise FileNotFoundError(f'{path} not found, please include on templates path')
424
+
425
+ return file_path
426
+
425
427
  def update(self):
426
428
  raise NotImplementedError
427
429
 
@@ -433,5 +435,6 @@ class Element(ElementConstrutor): # pragma: no cover
433
435
  else:
434
436
  return obj
435
437
 
438
+
436
439
  def __str__(self):
437
440
  return self.to_html()
@@ -1,14 +1,13 @@
1
1
  import os
2
2
  from uuid import uuid4
3
3
  from pyweber.core.element import Element, SEARCH_MODE
4
- from pyweber.utils.loads import LoadStaticFiles
5
4
  from pyweber.config.config import config
6
5
  from pyweber.utils.types import HTTPStatusCode, GetBy
7
6
 
8
7
  class Template: # pragma: no cover
9
8
  def __init__(self, template: str, status_code: int = 200, title: str = None, include_uuid: bool = True, **kwargs):
10
9
  self.__include_uuid = include_uuid
11
- self.__template = self.__read_file(file_path=template)
10
+ self.__template = Element.read_file(file_path=template)
12
11
  self.kwargs = kwargs
13
12
  self.data = None
14
13
  self.__status_code = status_code
@@ -135,22 +134,6 @@ class Template: # pragma: no cover
135
134
 
136
135
  return element
137
136
 
138
- def __read_file(self, file_path: str) -> str:
139
- from pyweber.models.error_pages import ErrorPages
140
- if file_path.endswith('.html'):
141
- path = os.path.join('templates', file_path) if not os.path.isfile(file_path) else file_path
142
-
143
- try:
144
- return LoadStaticFiles(path=path).load
145
-
146
- except FileNotFoundError:
147
- return ErrorPages().page_server_error.build_html().replace(
148
- "{{error}}",
149
- f'{path} not found, please include on templates file'
150
- )
151
-
152
- return file_path
153
-
154
137
  def __create_default_element(self, *args, **kwargs):
155
138
  return Element(*args, **kwargs)
156
139
 
@@ -369,8 +369,7 @@ class ElementConstrutor: # pragma: no cover
369
369
  else:
370
370
  child.remove_attr('selected')
371
371
 
372
- elif self.get_attr('type', None) == 'checkbox':
373
- self.__value = None
372
+ elif self.attrs.get('type', None) == 'checkbox':
374
373
  if value == 'on':
375
374
  self.set_attr('checked', '')
376
375
 
@@ -468,7 +467,7 @@ class ElementConstrutor: # pragma: no cover
468
467
  return html
469
468
 
470
469
  @classmethod
471
- def render_dynamic_values(self, content: str, **kwargs):
470
+ def render_dynamic_values(cls, content: str, **kwargs):
472
471
 
473
472
  if content:
474
473
  pattern = r'\{\{(.*?)\}\}'
@@ -479,7 +478,7 @@ class ElementConstrutor: # pragma: no cover
479
478
  value = kwargs.get(r.strip(), None)
480
479
  if value is not None:
481
480
  if isinstance(value, ElementConstrutor):
482
- value = self.to_html(element=value)
481
+ value = value.to_html(element=value)
483
482
 
484
483
  content = content.replace("{{" + r + "}}", str(value))
485
484
  return content
@@ -6,10 +6,10 @@ class TemplateDiff: # pragma: no cover
6
6
  def __init__(self):
7
7
  self.__differences: dict[str, dict[str, str]] = {}
8
8
  self.__checked_elements: list[Element] = []
9
-
9
+
10
10
  @property
11
11
  def differences(self): return self.__differences
12
-
12
+
13
13
  def __raise_typr_error(self, *elements: Element):
14
14
  for element in elements:
15
15
  if not isinstance(element, Element):
@@ -18,49 +18,39 @@ class TemplateDiff: # pragma: no cover
18
18
  def track_differences(self, new_element: Union[Element, Template], old_element: Union[Element, Template]):
19
19
  if isinstance(old_element, Template):
20
20
  old_element = old_element.root
21
-
21
+
22
22
  if isinstance(new_element, Template):
23
23
  new_element = new_element.root
24
-
24
+
25
25
  self.__raise_typr_error(old_element, new_element)
26
-
26
+
27
27
  status = None
28
28
  methods = new_element.get_element_methods()
29
-
29
+
30
30
  if new_element.uuid != old_element.uuid:
31
31
  status = 'Added'
32
-
33
32
  else:
34
33
  if new_element.id != old_element.id:
35
34
  status = 'Changed'
36
-
37
35
  elif new_element.content != old_element.content:
38
36
  status = 'Changed'
39
-
40
37
  elif new_element.value != old_element.value:
41
38
  status = 'Changed'
42
-
43
39
  elif new_element.tag != old_element.tag:
44
40
  status = 'Changed'
45
-
46
41
  elif new_element.attrs != old_element.attrs:
47
42
  status = 'Changed'
48
-
49
43
  elif new_element.style != old_element.style:
50
44
  status = 'Changed'
51
-
52
45
  elif new_element.events.__dict__ != old_element.events.__dict__:
53
46
  status = 'Changed'
54
-
55
47
  elif [v for v in new_element.classes if v not in old_element.classes]:
56
48
  status = 'Changed'
57
-
58
49
  elif [v for v in old_element.classes if v not in new_element.classes]:
59
50
  status = 'Changed'
60
-
61
51
  elif methods:
62
52
  status = 'Changed'
63
-
53
+
64
54
  if status:
65
55
  self.add_element_on_diff(element=new_element, status=status, methods=methods)
66
56
 
@@ -68,22 +58,27 @@ class TemplateDiff: # pragma: no cover
68
58
  self.add_element_on_diff(element=old_element, status='Removed', methods=methods)
69
59
 
70
60
  self.__checked_elements.append(new_element.uuid)
71
-
61
+
72
62
  new_element_childs_map = {child.uuid: child for child in new_element.childs}
73
63
  old_element_childs_map = {child.uuid: child for child in old_element.childs}
74
-
64
+
75
65
  for uuid, old_child in old_element_childs_map.items():
76
66
  if uuid in new_element_childs_map:
77
67
  if old_child.parent and old_child.parent.uuid not in self.__checked_elements:
78
68
  self.track_differences(new_element_childs_map[uuid], old_child)
79
-
80
69
  else:
81
- self.add_element_on_diff(element=old_child, status='Removed', methods=methods)
70
+ # ✅ só marca Removed se o pai NÃO está já no diff como Changed
71
+ parent_uuid = old_child.parent.uuid if old_child.parent else None
72
+ if parent_uuid not in self.__differences or self.__differences[parent_uuid]['status'] != 'Changed':
73
+ self.add_element_on_diff(element=old_child, status='Removed', methods=methods)
82
74
 
83
75
  for uuid, new_child in new_element_childs_map.items():
84
76
  if uuid not in old_element_childs_map:
85
- self.add_element_on_diff(element=new_child, status='Added', methods=methods)
86
-
77
+ # ✅ só marca Added se o pai NÃO está já no diff como Changed
78
+ parent_uuid = new_child.parent.uuid if new_child.parent else None
79
+ if parent_uuid not in self.__differences or self.__differences[parent_uuid]['status'] != 'Changed':
80
+ self.add_element_on_diff(element=new_child, status='Added', methods=methods)
81
+
87
82
  def add_element_on_diff(self, element: Element, status: Literal['Added', 'Changed', 'Removed'], methods: dict[str, dict[str, Any]]):
88
83
  self.differences[element.uuid] = {
89
84
  'parent': element.parent.uuid if element.parent else None,
@@ -94,4 +89,4 @@ class TemplateDiff: # pragma: no cover
94
89
  if status != 'Removed':
95
90
  self.differences[element.uuid]['methods'] = {**methods}
96
91
 
97
- element.remove_element_methods()
92
+ element.remove_element_methods()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyweber
3
- Version: 1.2.0.dev20260428
3
+ Version: 1.2.0.dev20260430
4
4
  Summary: A lightweight Python framework for building and managing web applications.
5
5
  Author-email: DevPythonMZ <pypi.dev@gmail.com>
6
6
  License-Expression: MIT