plain 0.38.0__tar.gz → 0.39.1__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 (164) hide show
  1. {plain-0.38.0 → plain-0.39.1}/PKG-INFO +1 -1
  2. plain-0.39.1/plain/chores/README.md +64 -0
  3. plain-0.39.1/plain/chores/__init__.py +3 -0
  4. plain-0.39.1/plain/chores/registry.py +79 -0
  5. plain-0.39.1/plain/cli/chores.py +91 -0
  6. {plain-0.38.0 → plain-0.39.1}/plain/cli/core.py +2 -0
  7. {plain-0.38.0 → plain-0.39.1}/plain/views/base.py +2 -11
  8. {plain-0.38.0 → plain-0.39.1}/pyproject.toml +1 -1
  9. {plain-0.38.0 → plain-0.39.1}/.gitignore +0 -0
  10. {plain-0.38.0 → plain-0.39.1}/LICENSE +0 -0
  11. {plain-0.38.0 → plain-0.39.1}/README.md +0 -0
  12. {plain-0.38.0 → plain-0.39.1}/plain/README.md +0 -0
  13. {plain-0.38.0 → plain-0.39.1}/plain/__main__.py +0 -0
  14. {plain-0.38.0 → plain-0.39.1}/plain/assets/README.md +0 -0
  15. {plain-0.38.0 → plain-0.39.1}/plain/assets/__init__.py +0 -0
  16. {plain-0.38.0 → plain-0.39.1}/plain/assets/compile.py +0 -0
  17. {plain-0.38.0 → plain-0.39.1}/plain/assets/finders.py +0 -0
  18. {plain-0.38.0 → plain-0.39.1}/plain/assets/fingerprints.py +0 -0
  19. {plain-0.38.0 → plain-0.39.1}/plain/assets/urls.py +0 -0
  20. {plain-0.38.0 → plain-0.39.1}/plain/assets/views.py +0 -0
  21. {plain-0.38.0 → plain-0.39.1}/plain/cli/README.md +0 -0
  22. {plain-0.38.0 → plain-0.39.1}/plain/cli/__init__.py +0 -0
  23. {plain-0.38.0 → plain-0.39.1}/plain/cli/build.py +0 -0
  24. {plain-0.38.0 → plain-0.39.1}/plain/cli/docs.py +0 -0
  25. {plain-0.38.0 → plain-0.39.1}/plain/cli/formatting.py +0 -0
  26. {plain-0.38.0 → plain-0.39.1}/plain/cli/preflight.py +0 -0
  27. {plain-0.38.0 → plain-0.39.1}/plain/cli/print.py +0 -0
  28. {plain-0.38.0 → plain-0.39.1}/plain/cli/registry.py +0 -0
  29. {plain-0.38.0 → plain-0.39.1}/plain/cli/scaffold.py +0 -0
  30. {plain-0.38.0 → plain-0.39.1}/plain/cli/settings.py +0 -0
  31. {plain-0.38.0 → plain-0.39.1}/plain/cli/shell.py +0 -0
  32. {plain-0.38.0 → plain-0.39.1}/plain/cli/startup.py +0 -0
  33. {plain-0.38.0 → plain-0.39.1}/plain/cli/urls.py +0 -0
  34. {plain-0.38.0 → plain-0.39.1}/plain/cli/utils.py +0 -0
  35. {plain-0.38.0 → plain-0.39.1}/plain/csrf/README.md +0 -0
  36. {plain-0.38.0 → plain-0.39.1}/plain/csrf/middleware.py +0 -0
  37. {plain-0.38.0 → plain-0.39.1}/plain/csrf/views.py +0 -0
  38. {plain-0.38.0 → plain-0.39.1}/plain/debug.py +0 -0
  39. {plain-0.38.0 → plain-0.39.1}/plain/exceptions.py +0 -0
  40. {plain-0.38.0 → plain-0.39.1}/plain/forms/README.md +0 -0
  41. {plain-0.38.0 → plain-0.39.1}/plain/forms/__init__.py +0 -0
  42. {plain-0.38.0 → plain-0.39.1}/plain/forms/boundfield.py +0 -0
  43. {plain-0.38.0 → plain-0.39.1}/plain/forms/exceptions.py +0 -0
  44. {plain-0.38.0 → plain-0.39.1}/plain/forms/fields.py +0 -0
  45. {plain-0.38.0 → plain-0.39.1}/plain/forms/forms.py +0 -0
  46. {plain-0.38.0 → plain-0.39.1}/plain/http/README.md +0 -0
  47. {plain-0.38.0 → plain-0.39.1}/plain/http/__init__.py +0 -0
  48. {plain-0.38.0 → plain-0.39.1}/plain/http/cookie.py +0 -0
  49. {plain-0.38.0 → plain-0.39.1}/plain/http/multipartparser.py +0 -0
  50. {plain-0.38.0 → plain-0.39.1}/plain/http/request.py +0 -0
  51. {plain-0.38.0 → plain-0.39.1}/plain/http/response.py +0 -0
  52. {plain-0.38.0 → plain-0.39.1}/plain/internal/__init__.py +0 -0
  53. {plain-0.38.0 → plain-0.39.1}/plain/internal/files/__init__.py +0 -0
  54. {plain-0.38.0 → plain-0.39.1}/plain/internal/files/base.py +0 -0
  55. {plain-0.38.0 → plain-0.39.1}/plain/internal/files/locks.py +0 -0
  56. {plain-0.38.0 → plain-0.39.1}/plain/internal/files/move.py +0 -0
  57. {plain-0.38.0 → plain-0.39.1}/plain/internal/files/temp.py +0 -0
  58. {plain-0.38.0 → plain-0.39.1}/plain/internal/files/uploadedfile.py +0 -0
  59. {plain-0.38.0 → plain-0.39.1}/plain/internal/files/uploadhandler.py +0 -0
  60. {plain-0.38.0 → plain-0.39.1}/plain/internal/files/utils.py +0 -0
  61. {plain-0.38.0 → plain-0.39.1}/plain/internal/handlers/__init__.py +0 -0
  62. {plain-0.38.0 → plain-0.39.1}/plain/internal/handlers/base.py +0 -0
  63. {plain-0.38.0 → plain-0.39.1}/plain/internal/handlers/exception.py +0 -0
  64. {plain-0.38.0 → plain-0.39.1}/plain/internal/handlers/wsgi.py +0 -0
  65. {plain-0.38.0 → plain-0.39.1}/plain/internal/middleware/__init__.py +0 -0
  66. {plain-0.38.0 → plain-0.39.1}/plain/internal/middleware/headers.py +0 -0
  67. {plain-0.38.0 → plain-0.39.1}/plain/internal/middleware/https.py +0 -0
  68. {plain-0.38.0 → plain-0.39.1}/plain/internal/middleware/slash.py +0 -0
  69. {plain-0.38.0 → plain-0.39.1}/plain/json.py +0 -0
  70. {plain-0.38.0 → plain-0.39.1}/plain/logs/README.md +0 -0
  71. {plain-0.38.0 → plain-0.39.1}/plain/logs/__init__.py +0 -0
  72. {plain-0.38.0 → plain-0.39.1}/plain/logs/configure.py +0 -0
  73. {plain-0.38.0 → plain-0.39.1}/plain/logs/loggers.py +0 -0
  74. {plain-0.38.0 → plain-0.39.1}/plain/logs/utils.py +0 -0
  75. {plain-0.38.0 → plain-0.39.1}/plain/packages/README.md +0 -0
  76. {plain-0.38.0 → plain-0.39.1}/plain/packages/__init__.py +0 -0
  77. {plain-0.38.0 → plain-0.39.1}/plain/packages/config.py +0 -0
  78. {plain-0.38.0 → plain-0.39.1}/plain/packages/registry.py +0 -0
  79. {plain-0.38.0 → plain-0.39.1}/plain/paginator.py +0 -0
  80. {plain-0.38.0 → plain-0.39.1}/plain/preflight/README.md +0 -0
  81. {plain-0.38.0 → plain-0.39.1}/plain/preflight/__init__.py +0 -0
  82. {plain-0.38.0 → plain-0.39.1}/plain/preflight/files.py +0 -0
  83. {plain-0.38.0 → plain-0.39.1}/plain/preflight/messages.py +0 -0
  84. {plain-0.38.0 → plain-0.39.1}/plain/preflight/registry.py +0 -0
  85. {plain-0.38.0 → plain-0.39.1}/plain/preflight/security.py +0 -0
  86. {plain-0.38.0 → plain-0.39.1}/plain/preflight/urls.py +0 -0
  87. {plain-0.38.0 → plain-0.39.1}/plain/runtime/README.md +0 -0
  88. {plain-0.38.0 → plain-0.39.1}/plain/runtime/__init__.py +0 -0
  89. {plain-0.38.0 → plain-0.39.1}/plain/runtime/global_settings.py +0 -0
  90. {plain-0.38.0 → plain-0.39.1}/plain/runtime/user_settings.py +0 -0
  91. {plain-0.38.0 → plain-0.39.1}/plain/signals/README.md +0 -0
  92. {plain-0.38.0 → plain-0.39.1}/plain/signals/__init__.py +0 -0
  93. {plain-0.38.0 → plain-0.39.1}/plain/signals/dispatch/__init__.py +0 -0
  94. {plain-0.38.0 → plain-0.39.1}/plain/signals/dispatch/dispatcher.py +0 -0
  95. {plain-0.38.0 → plain-0.39.1}/plain/signals/dispatch/license.txt +0 -0
  96. {plain-0.38.0 → plain-0.39.1}/plain/signing.py +0 -0
  97. {plain-0.38.0 → plain-0.39.1}/plain/templates/README.md +0 -0
  98. {plain-0.38.0 → plain-0.39.1}/plain/templates/__init__.py +0 -0
  99. {plain-0.38.0 → plain-0.39.1}/plain/templates/core.py +0 -0
  100. {plain-0.38.0 → plain-0.39.1}/plain/templates/jinja/__init__.py +0 -0
  101. {plain-0.38.0 → plain-0.39.1}/plain/templates/jinja/environments.py +0 -0
  102. {plain-0.38.0 → plain-0.39.1}/plain/templates/jinja/extensions.py +0 -0
  103. {plain-0.38.0 → plain-0.39.1}/plain/templates/jinja/filters.py +0 -0
  104. {plain-0.38.0 → plain-0.39.1}/plain/templates/jinja/globals.py +0 -0
  105. {plain-0.38.0 → plain-0.39.1}/plain/test/README.md +0 -0
  106. {plain-0.38.0 → plain-0.39.1}/plain/test/__init__.py +0 -0
  107. {plain-0.38.0 → plain-0.39.1}/plain/test/client.py +0 -0
  108. {plain-0.38.0 → plain-0.39.1}/plain/test/encoding.py +0 -0
  109. {plain-0.38.0 → plain-0.39.1}/plain/test/exceptions.py +0 -0
  110. {plain-0.38.0 → plain-0.39.1}/plain/urls/README.md +0 -0
  111. {plain-0.38.0 → plain-0.39.1}/plain/urls/__init__.py +0 -0
  112. {plain-0.38.0 → plain-0.39.1}/plain/urls/converters.py +0 -0
  113. {plain-0.38.0 → plain-0.39.1}/plain/urls/exceptions.py +0 -0
  114. {plain-0.38.0 → plain-0.39.1}/plain/urls/patterns.py +0 -0
  115. {plain-0.38.0 → plain-0.39.1}/plain/urls/resolvers.py +0 -0
  116. {plain-0.38.0 → plain-0.39.1}/plain/urls/routers.py +0 -0
  117. {plain-0.38.0 → plain-0.39.1}/plain/urls/utils.py +0 -0
  118. {plain-0.38.0 → plain-0.39.1}/plain/utils/README.md +0 -0
  119. {plain-0.38.0 → plain-0.39.1}/plain/utils/__init__.py +0 -0
  120. {plain-0.38.0 → plain-0.39.1}/plain/utils/cache.py +0 -0
  121. {plain-0.38.0 → plain-0.39.1}/plain/utils/connection.py +0 -0
  122. {plain-0.38.0 → plain-0.39.1}/plain/utils/crypto.py +0 -0
  123. {plain-0.38.0 → plain-0.39.1}/plain/utils/datastructures.py +0 -0
  124. {plain-0.38.0 → plain-0.39.1}/plain/utils/dateparse.py +0 -0
  125. {plain-0.38.0 → plain-0.39.1}/plain/utils/deconstruct.py +0 -0
  126. {plain-0.38.0 → plain-0.39.1}/plain/utils/decorators.py +0 -0
  127. {plain-0.38.0 → plain-0.39.1}/plain/utils/duration.py +0 -0
  128. {plain-0.38.0 → plain-0.39.1}/plain/utils/encoding.py +0 -0
  129. {plain-0.38.0 → plain-0.39.1}/plain/utils/functional.py +0 -0
  130. {plain-0.38.0 → plain-0.39.1}/plain/utils/hashable.py +0 -0
  131. {plain-0.38.0 → plain-0.39.1}/plain/utils/html.py +0 -0
  132. {plain-0.38.0 → plain-0.39.1}/plain/utils/http.py +0 -0
  133. {plain-0.38.0 → plain-0.39.1}/plain/utils/inspect.py +0 -0
  134. {plain-0.38.0 → plain-0.39.1}/plain/utils/ipv6.py +0 -0
  135. {plain-0.38.0 → plain-0.39.1}/plain/utils/itercompat.py +0 -0
  136. {plain-0.38.0 → plain-0.39.1}/plain/utils/module_loading.py +0 -0
  137. {plain-0.38.0 → plain-0.39.1}/plain/utils/regex_helper.py +0 -0
  138. {plain-0.38.0 → plain-0.39.1}/plain/utils/safestring.py +0 -0
  139. {plain-0.38.0 → plain-0.39.1}/plain/utils/text.py +0 -0
  140. {plain-0.38.0 → plain-0.39.1}/plain/utils/timesince.py +0 -0
  141. {plain-0.38.0 → plain-0.39.1}/plain/utils/timezone.py +0 -0
  142. {plain-0.38.0 → plain-0.39.1}/plain/utils/tree.py +0 -0
  143. {plain-0.38.0 → plain-0.39.1}/plain/validators.py +0 -0
  144. {plain-0.38.0 → plain-0.39.1}/plain/views/README.md +0 -0
  145. {plain-0.38.0 → plain-0.39.1}/plain/views/__init__.py +0 -0
  146. {plain-0.38.0 → plain-0.39.1}/plain/views/csrf.py +0 -0
  147. {plain-0.38.0 → plain-0.39.1}/plain/views/errors.py +0 -0
  148. {plain-0.38.0 → plain-0.39.1}/plain/views/exceptions.py +0 -0
  149. {plain-0.38.0 → plain-0.39.1}/plain/views/forms.py +0 -0
  150. {plain-0.38.0 → plain-0.39.1}/plain/views/objects.py +0 -0
  151. {plain-0.38.0 → plain-0.39.1}/plain/views/redirect.py +0 -0
  152. {plain-0.38.0 → plain-0.39.1}/plain/views/templates.py +0 -0
  153. {plain-0.38.0 → plain-0.39.1}/plain/wsgi.py +0 -0
  154. {plain-0.38.0 → plain-0.39.1}/tests/.bolt/assets_collected/assets.json +0 -0
  155. {plain-0.38.0 → plain-0.39.1}/tests/.gitignore +0 -0
  156. {plain-0.38.0 → plain-0.39.1}/tests/app/.gitignore +0 -0
  157. {plain-0.38.0 → plain-0.39.1}/tests/app/settings.py +0 -0
  158. {plain-0.38.0 → plain-0.39.1}/tests/app/test/__init__.py +0 -0
  159. {plain-0.38.0 → plain-0.39.1}/tests/app/test/default_settings.py +0 -0
  160. {plain-0.38.0 → plain-0.39.1}/tests/app/urls.py +0 -0
  161. {plain-0.38.0 → plain-0.39.1}/tests/conftest.py +0 -0
  162. {plain-0.38.0 → plain-0.39.1}/tests/test_cli.py +0 -0
  163. {plain-0.38.0 → plain-0.39.1}/tests/test_runtime.py +0 -0
  164. {plain-0.38.0 → plain-0.39.1}/tests/test_wsgi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.38.0
3
+ Version: 0.39.1
4
4
  Summary: A web framework for building products with Python.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -0,0 +1,64 @@
1
+ # Chores
2
+
3
+ **Routine maintenance tasks.**
4
+
5
+ Chores are registered functions that can be run at any time to keep an app in a desirable state.
6
+
7
+ ![](https://assets.plainframework.com/docs/plain-chores-run.png)
8
+
9
+ A good example is the clearing of expired sessions in [`plain.sessions`](/plain-sessions/plain/sessions/chores.py) — since the sessions are stored in the database, occasionally you will want to delete any sessions that are expired and no longer in use.
10
+
11
+ ```python
12
+ # plain/sessions/chores.py
13
+ from plain.chores import register_chore
14
+ from plain.utils import timezone
15
+
16
+ from .models import Session
17
+
18
+
19
+ @register_chore("sessions")
20
+ def clear_expired():
21
+ """
22
+ Delete sessions that have expired.
23
+ """
24
+ result = Session.objects.filter(expires_at__lt=timezone.now()).delete()
25
+ return f"{result[0]} expired sessions deleted"
26
+ ```
27
+
28
+ ## Running chores
29
+
30
+ The `plain chores run` command will execute all registered chores. When and how to run this is up to the user, but running them hourly is a safe assumption in most cases (assuming you have any chores — `plain chores list`).
31
+
32
+ There are several ways you can run chores depending on your needs:
33
+
34
+ - on deploy
35
+ - as a [`plain.worker` scheduled job](/plain-worker/plain/worker/README.md#scheduled-jobs)
36
+ - as a cron job (using any cron-like system where your app is hosted)
37
+ - manually as needed
38
+
39
+ ## Writing chores
40
+
41
+ A chore is a function decorated with `@register_chore(chore_group_name)`. It can write a description as a docstring, and it can return a value that will be printed when the chore is run.
42
+
43
+ ```python
44
+ # app/chores.py
45
+ from plain.chores import register_chore
46
+
47
+
48
+ @register_chore("app")
49
+ def chore_name():
50
+ """
51
+ A chore description can go here
52
+ """
53
+ # Do a thing!
54
+ return "We did it!"
55
+ ```
56
+
57
+ A good chore is:
58
+
59
+ - Fast
60
+ - Idempotent
61
+ - Recurring
62
+ - Stateless
63
+
64
+ If chores are written in `app/chores.py` or `{pkg}/chores.py`, then they will be imported automatically and registered.
@@ -0,0 +1,3 @@
1
+ from .registry import register_chore
2
+
3
+ __all__ = ["register_chore"]
@@ -0,0 +1,79 @@
1
+ from importlib import import_module
2
+ from importlib.util import find_spec
3
+
4
+ from plain.packages import packages_registry
5
+
6
+
7
+ class Chore:
8
+ def __init__(self, *, group, func):
9
+ self.group = group
10
+ self.func = func
11
+ self.name = f"{group}.{func.__name__}"
12
+ self.description = func.__doc__.strip() if func.__doc__ else ""
13
+
14
+ def __str__(self):
15
+ return self.name
16
+
17
+ def run(self):
18
+ """
19
+ Run the chore.
20
+ """
21
+ return self.func()
22
+
23
+
24
+ class ChoresRegistry:
25
+ def __init__(self):
26
+ self._chores = {}
27
+
28
+ def register_chore(self, chore):
29
+ """
30
+ Register a chore with the specified name.
31
+ """
32
+ self._chores[chore.func] = chore
33
+
34
+ def import_modules(self):
35
+ """
36
+ Import modules from installed packages and app to trigger registration.
37
+ """
38
+ # Import from installed packages
39
+ for package_config in packages_registry.get_package_configs():
40
+ import_name = f"{package_config.name}.chores"
41
+ try:
42
+ import_module(import_name)
43
+ except ModuleNotFoundError:
44
+ pass
45
+
46
+ # Import from app
47
+ import_name = "app.chores"
48
+ if find_spec(import_name):
49
+ try:
50
+ import_module(import_name)
51
+ except ModuleNotFoundError:
52
+ pass
53
+
54
+ def get_chores(self):
55
+ """
56
+ Get all registered chores.
57
+ """
58
+ return list(self._chores.values())
59
+
60
+
61
+ chores_registry = ChoresRegistry()
62
+
63
+
64
+ def register_chore(group):
65
+ """
66
+ Register a chore with a given group.
67
+
68
+ Usage:
69
+ @register_chore("clear_expired")
70
+ def clear_expired():
71
+ pass
72
+ """
73
+
74
+ def wrapper(func):
75
+ chore = Chore(group=group, func=func)
76
+ chores_registry.register_chore(chore)
77
+ return func
78
+
79
+ return wrapper
@@ -0,0 +1,91 @@
1
+ import logging
2
+ import sys
3
+
4
+ import click
5
+
6
+ logger = logging.getLogger("plain.chores")
7
+
8
+
9
+ @click.group()
10
+ def chores():
11
+ """Routine maintenance tasks"""
12
+ pass
13
+
14
+
15
+ @chores.command("list")
16
+ @click.option("--group", default=None, type=str, help="Group to run", multiple=True)
17
+ @click.option(
18
+ "--name", default=None, type=str, help="Name of the chore to run", multiple=True
19
+ )
20
+ def list_chores(group, name):
21
+ """
22
+ List all registered chores.
23
+ """
24
+ from plain.chores.registry import chores_registry
25
+
26
+ chores_registry.import_modules()
27
+
28
+ if group or name:
29
+ chores = [
30
+ chore
31
+ for chore in chores_registry.get_chores()
32
+ if (chore.group in group or not group) and (chore.name in name or not name)
33
+ ]
34
+ else:
35
+ chores = chores_registry.get_chores()
36
+
37
+ for chore in chores:
38
+ click.secho(f"{chore}", bold=True, nl=False)
39
+ if chore.description:
40
+ click.echo(f": {chore.description}")
41
+ else:
42
+ click.echo("")
43
+
44
+
45
+ @chores.command("run")
46
+ @click.option("--group", default=None, type=str, help="Group to run", multiple=True)
47
+ @click.option(
48
+ "--name", default=None, type=str, help="Name of the chore to run", multiple=True
49
+ )
50
+ @click.option(
51
+ "--dry-run", is_flag=True, help="Show what would be done without executing"
52
+ )
53
+ def run_chores(group, name, dry_run):
54
+ """
55
+ Run the specified chores.
56
+ """
57
+ from plain.chores.registry import chores_registry
58
+
59
+ chores_registry.import_modules()
60
+
61
+ if group or name:
62
+ chores = [
63
+ chore
64
+ for chore in chores_registry.get_chores()
65
+ if (chore.group in group or not group) and (chore.name in name or not name)
66
+ ]
67
+ else:
68
+ chores = chores_registry.get_chores()
69
+
70
+ chores_failed = []
71
+
72
+ for chore in chores:
73
+ click.echo(f"{chore.name}:", nl=False)
74
+ if dry_run:
75
+ click.echo(" (dry run)", fg="yellow")
76
+ else:
77
+ try:
78
+ result = chore.run()
79
+ except Exception:
80
+ click.secho(" Failed", fg="red")
81
+ chores_failed.append(chore)
82
+ logger.exception(f"Error running chore {chore.name}")
83
+ continue
84
+
85
+ if result is None:
86
+ click.secho(" Done", fg="green")
87
+ else:
88
+ click.secho(f" {result}", fg="green")
89
+
90
+ if chores_failed:
91
+ sys.exit(1)
@@ -7,6 +7,7 @@ import plain.runtime
7
7
  from plain.exceptions import ImproperlyConfigured
8
8
 
9
9
  from .build import build
10
+ from .chores import chores
10
11
  from .docs import docs
11
12
  from .formatting import PlainContext
12
13
  from .preflight import preflight_checks
@@ -26,6 +27,7 @@ def plain_cli():
26
27
  plain_cli.add_command(docs)
27
28
  plain_cli.add_command(preflight_checks)
28
29
  plain_cli.add_command(create)
30
+ plain_cli.add_command(chores)
29
31
  plain_cli.add_command(build)
30
32
  plain_cli.add_command(utils)
31
33
  plain_cli.add_command(urls)
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from http import HTTPMethod
2
3
 
3
4
  from plain.http import (
4
5
  HttpRequest,
@@ -100,14 +101,4 @@ class View:
100
101
  return response
101
102
 
102
103
  def _allowed_methods(self) -> list[str]:
103
- known_http_methods = [
104
- "get",
105
- "post",
106
- "put",
107
- "patch",
108
- "delete",
109
- "head",
110
- "options",
111
- "trace",
112
- ]
113
- return [m.upper() for m in known_http_methods if hasattr(self, m)]
104
+ return [m.upper() for m in HTTPMethod if hasattr(self, m.lower())]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain"
3
- version = "0.38.0"
3
+ version = "0.39.1"
4
4
  description = "A web framework for building products with Python."
5
5
  authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
6
6
  readme = "README.md"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes