plain 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.
Files changed (169) hide show
  1. plain/README.md +33 -0
  2. plain/__main__.py +5 -0
  3. plain/assets/README.md +56 -0
  4. plain/assets/__init__.py +6 -0
  5. plain/assets/finders.py +233 -0
  6. plain/assets/preflight.py +14 -0
  7. plain/assets/storage.py +916 -0
  8. plain/assets/utils.py +52 -0
  9. plain/assets/whitenoise/__init__.py +5 -0
  10. plain/assets/whitenoise/base.py +259 -0
  11. plain/assets/whitenoise/compress.py +189 -0
  12. plain/assets/whitenoise/media_types.py +137 -0
  13. plain/assets/whitenoise/middleware.py +197 -0
  14. plain/assets/whitenoise/responders.py +286 -0
  15. plain/assets/whitenoise/storage.py +178 -0
  16. plain/assets/whitenoise/string_utils.py +13 -0
  17. plain/cli/README.md +123 -0
  18. plain/cli/__init__.py +3 -0
  19. plain/cli/cli.py +439 -0
  20. plain/cli/formatting.py +61 -0
  21. plain/cli/packages.py +73 -0
  22. plain/cli/print.py +9 -0
  23. plain/cli/startup.py +33 -0
  24. plain/csrf/README.md +3 -0
  25. plain/csrf/middleware.py +466 -0
  26. plain/csrf/views.py +10 -0
  27. plain/debug.py +23 -0
  28. plain/exceptions.py +242 -0
  29. plain/forms/README.md +14 -0
  30. plain/forms/__init__.py +8 -0
  31. plain/forms/boundfield.py +58 -0
  32. plain/forms/exceptions.py +11 -0
  33. plain/forms/fields.py +1030 -0
  34. plain/forms/forms.py +297 -0
  35. plain/http/README.md +1 -0
  36. plain/http/__init__.py +51 -0
  37. plain/http/cookie.py +20 -0
  38. plain/http/multipartparser.py +743 -0
  39. plain/http/request.py +754 -0
  40. plain/http/response.py +719 -0
  41. plain/internal/__init__.py +0 -0
  42. plain/internal/files/README.md +3 -0
  43. plain/internal/files/__init__.py +3 -0
  44. plain/internal/files/base.py +161 -0
  45. plain/internal/files/locks.py +127 -0
  46. plain/internal/files/move.py +102 -0
  47. plain/internal/files/temp.py +79 -0
  48. plain/internal/files/uploadedfile.py +150 -0
  49. plain/internal/files/uploadhandler.py +254 -0
  50. plain/internal/files/utils.py +78 -0
  51. plain/internal/handlers/__init__.py +0 -0
  52. plain/internal/handlers/base.py +133 -0
  53. plain/internal/handlers/exception.py +145 -0
  54. plain/internal/handlers/wsgi.py +216 -0
  55. plain/internal/legacy/__init__.py +0 -0
  56. plain/internal/legacy/__main__.py +12 -0
  57. plain/internal/legacy/management/__init__.py +414 -0
  58. plain/internal/legacy/management/base.py +692 -0
  59. plain/internal/legacy/management/color.py +113 -0
  60. plain/internal/legacy/management/commands/__init__.py +0 -0
  61. plain/internal/legacy/management/commands/collectstatic.py +297 -0
  62. plain/internal/legacy/management/sql.py +67 -0
  63. plain/internal/legacy/management/utils.py +175 -0
  64. plain/json.py +40 -0
  65. plain/logs/README.md +24 -0
  66. plain/logs/__init__.py +5 -0
  67. plain/logs/configure.py +39 -0
  68. plain/logs/loggers.py +74 -0
  69. plain/logs/utils.py +46 -0
  70. plain/middleware/README.md +3 -0
  71. plain/middleware/__init__.py +0 -0
  72. plain/middleware/clickjacking.py +52 -0
  73. plain/middleware/common.py +87 -0
  74. plain/middleware/gzip.py +64 -0
  75. plain/middleware/security.py +64 -0
  76. plain/packages/README.md +41 -0
  77. plain/packages/__init__.py +4 -0
  78. plain/packages/config.py +259 -0
  79. plain/packages/registry.py +438 -0
  80. plain/paginator.py +187 -0
  81. plain/preflight/README.md +3 -0
  82. plain/preflight/__init__.py +38 -0
  83. plain/preflight/compatibility/__init__.py +0 -0
  84. plain/preflight/compatibility/django_4_0.py +20 -0
  85. plain/preflight/files.py +19 -0
  86. plain/preflight/messages.py +88 -0
  87. plain/preflight/registry.py +72 -0
  88. plain/preflight/security/__init__.py +0 -0
  89. plain/preflight/security/base.py +268 -0
  90. plain/preflight/security/csrf.py +40 -0
  91. plain/preflight/urls.py +117 -0
  92. plain/runtime/README.md +75 -0
  93. plain/runtime/__init__.py +61 -0
  94. plain/runtime/global_settings.py +199 -0
  95. plain/runtime/user_settings.py +353 -0
  96. plain/signals/README.md +14 -0
  97. plain/signals/__init__.py +5 -0
  98. plain/signals/dispatch/__init__.py +9 -0
  99. plain/signals/dispatch/dispatcher.py +320 -0
  100. plain/signals/dispatch/license.txt +35 -0
  101. plain/signing.py +299 -0
  102. plain/templates/README.md +20 -0
  103. plain/templates/__init__.py +6 -0
  104. plain/templates/core.py +24 -0
  105. plain/templates/jinja/README.md +227 -0
  106. plain/templates/jinja/__init__.py +22 -0
  107. plain/templates/jinja/defaults.py +119 -0
  108. plain/templates/jinja/extensions.py +39 -0
  109. plain/templates/jinja/filters.py +28 -0
  110. plain/templates/jinja/globals.py +19 -0
  111. plain/test/README.md +3 -0
  112. plain/test/__init__.py +16 -0
  113. plain/test/client.py +985 -0
  114. plain/test/utils.py +255 -0
  115. plain/urls/README.md +3 -0
  116. plain/urls/__init__.py +40 -0
  117. plain/urls/base.py +118 -0
  118. plain/urls/conf.py +94 -0
  119. plain/urls/converters.py +66 -0
  120. plain/urls/exceptions.py +9 -0
  121. plain/urls/resolvers.py +731 -0
  122. plain/utils/README.md +3 -0
  123. plain/utils/__init__.py +0 -0
  124. plain/utils/_os.py +52 -0
  125. plain/utils/cache.py +327 -0
  126. plain/utils/connection.py +84 -0
  127. plain/utils/crypto.py +76 -0
  128. plain/utils/datastructures.py +345 -0
  129. plain/utils/dateformat.py +329 -0
  130. plain/utils/dateparse.py +154 -0
  131. plain/utils/dates.py +76 -0
  132. plain/utils/deconstruct.py +54 -0
  133. plain/utils/decorators.py +90 -0
  134. plain/utils/deprecation.py +6 -0
  135. plain/utils/duration.py +44 -0
  136. plain/utils/email.py +12 -0
  137. plain/utils/encoding.py +235 -0
  138. plain/utils/functional.py +456 -0
  139. plain/utils/hashable.py +26 -0
  140. plain/utils/html.py +401 -0
  141. plain/utils/http.py +374 -0
  142. plain/utils/inspect.py +73 -0
  143. plain/utils/ipv6.py +46 -0
  144. plain/utils/itercompat.py +8 -0
  145. plain/utils/module_loading.py +69 -0
  146. plain/utils/regex_helper.py +353 -0
  147. plain/utils/safestring.py +72 -0
  148. plain/utils/termcolors.py +221 -0
  149. plain/utils/text.py +518 -0
  150. plain/utils/timesince.py +138 -0
  151. plain/utils/timezone.py +244 -0
  152. plain/utils/tree.py +126 -0
  153. plain/validators.py +603 -0
  154. plain/views/README.md +268 -0
  155. plain/views/__init__.py +18 -0
  156. plain/views/base.py +107 -0
  157. plain/views/csrf.py +24 -0
  158. plain/views/errors.py +25 -0
  159. plain/views/exceptions.py +4 -0
  160. plain/views/forms.py +76 -0
  161. plain/views/objects.py +229 -0
  162. plain/views/redirect.py +72 -0
  163. plain/views/templates.py +66 -0
  164. plain/wsgi.py +11 -0
  165. plain-0.1.0.dist-info/LICENSE +85 -0
  166. plain-0.1.0.dist-info/METADATA +51 -0
  167. plain-0.1.0.dist-info/RECORD +169 -0
  168. plain-0.1.0.dist-info/WHEEL +4 -0
  169. plain-0.1.0.dist-info/entry_points.txt +3 -0
plain/views/objects.py ADDED
@@ -0,0 +1,229 @@
1
+ from plain.exceptions import ImproperlyConfigured, ObjectDoesNotExist
2
+ from plain.http import Http404, Response, ResponseRedirect
3
+
4
+ from .forms import FormView
5
+ from .templates import TemplateView
6
+
7
+
8
+ class ObjectTemplateViewMixin:
9
+ context_object_name = ""
10
+
11
+ def get(self) -> Response:
12
+ self.load_object()
13
+ return self.render_template()
14
+
15
+ def load_object(self) -> None:
16
+ try:
17
+ self.object = self.get_object()
18
+ except ObjectDoesNotExist:
19
+ raise Http404
20
+
21
+ if not self.object:
22
+ # Also raise 404 if the object is None
23
+ raise Http404
24
+
25
+ def get_object(self): # Intentionally untyped... subclasses must override this.
26
+ raise NotImplementedError(
27
+ f"get_object() is not implemented on {self.__class__.__name__}"
28
+ )
29
+
30
+ def get_template_context(self) -> dict:
31
+ """Insert the single object into the context dict."""
32
+ context = super().get_template_context() # type: ignore
33
+ context["object"] = self.object
34
+ if self.context_object_name:
35
+ context[self.context_object_name] = self.object
36
+ elif hasattr(self.object, "_meta"):
37
+ context[self.object._meta.model_name] = self.object
38
+ return context
39
+
40
+ def get_template_names(self) -> list[str]:
41
+ """
42
+ Return a list of template names to be used for the request. May not be
43
+ called if render_to_response() is overridden. Return the following list:
44
+
45
+ * the value of ``template_name`` on the view (if provided)
46
+ object instance that the view is operating upon (if available)
47
+ * ``<package_label>/<model_name><template_name_suffix>.html``
48
+ """
49
+ if self.template_name: # type: ignore
50
+ return [self.template_name] # type: ignore
51
+
52
+ # If template_name isn't specified, it's not a problem --
53
+ # we just start with an empty list.
54
+ names = []
55
+
56
+ # The least-specific option is the default <app>/<model>_detail.html;
57
+ # only use this if the object in question is a model.
58
+ if hasattr(self.object, "_meta"):
59
+ object_meta = self.object._meta
60
+ names.append(
61
+ f"{object_meta.package_label}/{object_meta.model_name}{self.template_name_suffix}.html"
62
+ )
63
+
64
+ return names
65
+
66
+
67
+ class DetailView(ObjectTemplateViewMixin, TemplateView):
68
+ """
69
+ Render a "detail" view of an object.
70
+
71
+ By default this is a model instance looked up from `self.queryset`, but the
72
+ view will support display of *any* object by overriding `self.get_object()`.
73
+ """
74
+
75
+ template_name_suffix = "_detail"
76
+
77
+
78
+ class CreateView(ObjectTemplateViewMixin, FormView):
79
+ """
80
+ View for creating a new object, with a response rendered by a template.
81
+ """
82
+
83
+ def post(self) -> Response:
84
+ """
85
+ Handle POST requests: instantiate a form instance with the passed
86
+ POST variables and then check if it's valid.
87
+ """
88
+ # Context expects self.object to exist
89
+ self.load_object()
90
+ return super().post()
91
+
92
+ def load_object(self) -> None:
93
+ self.object = None
94
+
95
+ # TODO? would rather you have to specify this...
96
+ def get_success_url(self):
97
+ """Return the URL to redirect to after processing a valid form."""
98
+ if self.success_url:
99
+ url = self.success_url.format(**self.object.__dict__)
100
+ else:
101
+ try:
102
+ url = self.object.get_absolute_url()
103
+ except AttributeError:
104
+ raise ImproperlyConfigured(
105
+ "No URL to redirect to. Either provide a url or define"
106
+ " a get_absolute_url method on the Model."
107
+ )
108
+ return url
109
+
110
+ def form_valid(self, form):
111
+ """If the form is valid, save the associated model."""
112
+ self.object = form.save()
113
+ return super().form_valid(form)
114
+
115
+
116
+ class UpdateView(ObjectTemplateViewMixin, FormView):
117
+ """View for updating an object, with a response rendered by a template."""
118
+
119
+ template_name_suffix = "_form"
120
+
121
+ def post(self) -> Response:
122
+ """
123
+ Handle POST requests: instantiate a form instance with the passed
124
+ POST variables and then check if it's valid.
125
+ """
126
+ self.load_object()
127
+ return super().post()
128
+
129
+ def get_success_url(self):
130
+ """Return the URL to redirect to after processing a valid form."""
131
+ if self.success_url:
132
+ url = self.success_url.format(**self.object.__dict__)
133
+ else:
134
+ try:
135
+ url = self.object.get_absolute_url()
136
+ except AttributeError:
137
+ raise ImproperlyConfigured(
138
+ "No URL to redirect to. Either provide a url or define"
139
+ " a get_absolute_url method on the Model."
140
+ )
141
+ return url
142
+
143
+ def form_valid(self, form):
144
+ """If the form is valid, save the associated model."""
145
+ self.object = form.save()
146
+ return super().form_valid(form)
147
+
148
+ def get_form_kwargs(self):
149
+ """Return the keyword arguments for instantiating the form."""
150
+ kwargs = super().get_form_kwargs()
151
+ kwargs.update({"instance": self.object})
152
+ return kwargs
153
+
154
+
155
+ class DeleteView(ObjectTemplateViewMixin, TemplateView):
156
+ """
157
+ View for deleting an object retrieved with self.get_object(), with a
158
+ response rendered by a template.
159
+ """
160
+
161
+ template_name_suffix = "_confirm_delete"
162
+
163
+ def get_form_kwargs(self):
164
+ """Return the keyword arguments for instantiating the form."""
165
+ kwargs = super().get_form_kwargs()
166
+ kwargs.update({"instance": self.object})
167
+ return kwargs
168
+
169
+ def get_success_url(self):
170
+ if self.success_url:
171
+ return self.success_url.format(**self.object.__dict__)
172
+ else:
173
+ raise ImproperlyConfigured("No URL to redirect to. Provide a success_url.")
174
+
175
+ def post(self):
176
+ self.load_object()
177
+ self.object.delete()
178
+ return ResponseRedirect(self.get_success_url())
179
+
180
+
181
+ class ListView(TemplateView):
182
+ """
183
+ Render some list of objects, set by `self.get_queryset()`, with a response
184
+ rendered by a template.
185
+ """
186
+
187
+ template_name_suffix = "_list"
188
+ context_object_name = "objects"
189
+
190
+ def get(self) -> Response:
191
+ self.objects = self.get_objects()
192
+ return super().get()
193
+
194
+ def get_objects(self):
195
+ raise NotImplementedError(
196
+ f"get_objects() is not implemented on {self.__class__.__name__}"
197
+ )
198
+
199
+ def get_template_context(self) -> dict:
200
+ """Insert the single object into the context dict."""
201
+ context = super().get_template_context() # type: ignore
202
+ context[self.context_object_name] = self.objects
203
+ return context
204
+
205
+ def get_template_names(self) -> list[str]:
206
+ """
207
+ Return a list of template names to be used for the request. May not be
208
+ called if render_to_response() is overridden. Return the following list:
209
+
210
+ * the value of ``template_name`` on the view (if provided)
211
+ object instance that the view is operating upon (if available)
212
+ * ``<package_label>/<model_name><template_name_suffix>.html``
213
+ """
214
+ if self.template_name: # type: ignore
215
+ return [self.template_name] # type: ignore
216
+
217
+ # If template_name isn't specified, it's not a problem --
218
+ # we just start with an empty list.
219
+ names = []
220
+
221
+ # The least-specific option is the default <app>/<model>_detail.html;
222
+ # only use this if the object in question is a model.
223
+ if hasattr(self.objects, "model") and hasattr(self.objects.model, "_meta"):
224
+ object_meta = self.objects.model._meta
225
+ names.append(
226
+ f"{object_meta.package_label}/{object_meta.model_name}{self.template_name_suffix}.html"
227
+ )
228
+
229
+ return names
@@ -0,0 +1,72 @@
1
+ import logging
2
+
3
+ from plain.http import (
4
+ ResponseGone,
5
+ ResponsePermanentRedirect,
6
+ ResponseRedirect,
7
+ )
8
+ from plain.urls import reverse
9
+
10
+ from .base import View
11
+
12
+ logger = logging.getLogger("plain.request")
13
+
14
+
15
+ class RedirectView(View):
16
+ """Provide a redirect on any GET request."""
17
+
18
+ permanent = False
19
+ url: str | None = None
20
+ pattern_name: str | None = None
21
+ query_string = False
22
+
23
+ def get_redirect_url(self):
24
+ """
25
+ Return the URL redirect to. Keyword arguments from the URL pattern
26
+ match generating the redirect request are provided as kwargs to this
27
+ method.
28
+ """
29
+ if self.url:
30
+ url = self.url % self.url_kwargs
31
+ elif self.pattern_name:
32
+ url = reverse(self.pattern_name, args=self.url_args, kwargs=self.url_kwargs)
33
+ else:
34
+ return None
35
+
36
+ args = self.request.META.get("QUERY_STRING", "")
37
+ if args and self.query_string:
38
+ url = f"{url}?{args}"
39
+ return url
40
+
41
+ def get(self):
42
+ url = self.get_redirect_url()
43
+ if url:
44
+ if self.permanent:
45
+ return ResponsePermanentRedirect(url)
46
+ else:
47
+ return ResponseRedirect(url)
48
+ else:
49
+ logger.warning(
50
+ "Gone: %s",
51
+ self.request.path,
52
+ extra={"status_code": 410, "request": self.request},
53
+ )
54
+ return ResponseGone()
55
+
56
+ def head(self):
57
+ return self.get()
58
+
59
+ def post(self):
60
+ return self.get()
61
+
62
+ def options(self):
63
+ return self.get()
64
+
65
+ def delete(self):
66
+ return self.get()
67
+
68
+ def put(self):
69
+ return self.get()
70
+
71
+ def patch(self):
72
+ return self.get()
@@ -0,0 +1,66 @@
1
+ from plain.csrf.middleware import get_token
2
+ from plain.exceptions import ImproperlyConfigured
3
+ from plain.runtime import settings
4
+ from plain.templates import Template, TemplateFileMissing
5
+ from plain.utils.functional import lazy
6
+ from plain.utils.html import format_html
7
+ from plain.utils.safestring import SafeString
8
+
9
+ from .base import View
10
+
11
+
12
+ def csrf_input(request):
13
+ return format_html(
14
+ '<input type="hidden" name="csrfmiddlewaretoken" value="{}">',
15
+ get_token(request),
16
+ )
17
+
18
+
19
+ csrf_input_lazy = lazy(csrf_input, SafeString, str)
20
+ csrf_token_lazy = lazy(get_token, str)
21
+
22
+
23
+ class TemplateView(View):
24
+ """
25
+ Render a template. Pass keyword arguments from the URLconf to the context.
26
+ """
27
+
28
+ template_name: str | None = None
29
+
30
+ def get_template_context(self) -> dict:
31
+ return {
32
+ "request": self.request,
33
+ "csrf_input": csrf_input_lazy(self.request),
34
+ "csrf_token": csrf_token_lazy(self.request),
35
+ "DEBUG": settings.DEBUG,
36
+ }
37
+
38
+ def get_template_names(self) -> list[str]:
39
+ """
40
+ Return a list of template names to be used for the request. Must return
41
+ a list. May not be called if render_to_response() is overridden.
42
+ """
43
+ if self.template_name is None:
44
+ raise ImproperlyConfigured(
45
+ "TemplateView requires either a definition of "
46
+ "'template_name' or an implementation of 'get_template_names()'"
47
+ )
48
+ else:
49
+ return [self.template_name]
50
+
51
+ def get_template(self) -> Template:
52
+ template_names = self.get_template_names()
53
+
54
+ for template_name in template_names:
55
+ try:
56
+ return Template(template_name)
57
+ except TemplateFileMissing:
58
+ pass
59
+
60
+ raise TemplateFileMissing(template_names)
61
+
62
+ def render_template(self) -> str:
63
+ return self.get_template().render(self.get_template_context())
64
+
65
+ def get(self):
66
+ return self.render_template()
plain/wsgi.py ADDED
@@ -0,0 +1,11 @@
1
+ import plain.runtime
2
+ from plain.internal.handlers.wsgi import WSGIHandler
3
+
4
+
5
+ def _get_wsgi_application():
6
+ plain.runtime.setup()
7
+ return WSGIHandler()
8
+
9
+
10
+ # The default `plain.wsgi:app` WSGI application
11
+ app = _get_wsgi_application()
@@ -0,0 +1,85 @@
1
+ ## Plain is released under the BSD 3-Clause License
2
+
3
+ BSD 3-Clause License
4
+
5
+ Copyright (c) 2023, Dropseed, LLC
6
+
7
+ Redistribution and use in source and binary forms, with or without
8
+ modification, are permitted provided that the following conditions are met:
9
+
10
+ 1. Redistributions of source code must retain the above copyright notice, this
11
+ list of conditions and the following disclaimer.
12
+
13
+ 2. Redistributions in binary form must reproduce the above copyright notice,
14
+ this list of conditions and the following disclaimer in the documentation
15
+ and/or other materials provided with the distribution.
16
+
17
+ 3. Neither the name of the copyright holder nor the names of its
18
+ contributors may be used to endorse or promote products derived from
19
+ this software without specific prior written permission.
20
+
21
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+
32
+
33
+ ## This package contains code forked from github.com/django/django
34
+
35
+ Copyright (c) Django Software Foundation and individual contributors.
36
+ All rights reserved.
37
+
38
+ Redistribution and use in source and binary forms, with or without modification,
39
+ are permitted provided that the following conditions are met:
40
+
41
+ 1. Redistributions of source code must retain the above copyright notice,
42
+ this list of conditions and the following disclaimer.
43
+
44
+ 2. Redistributions in binary form must reproduce the above copyright
45
+ notice, this list of conditions and the following disclaimer in the
46
+ documentation and/or other materials provided with the distribution.
47
+
48
+ 3. Neither the name of Django nor the names of its contributors may be used
49
+ to endorse or promote products derived from this software without
50
+ specific prior written permission.
51
+
52
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
53
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
54
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
55
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
56
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
57
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
58
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
59
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
60
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
61
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
62
+
63
+
64
+ ## This package contains code forked from github.com/evansd/whitenoise
65
+
66
+ The MIT License (MIT)
67
+
68
+ Copyright (c) 2013 David Evans
69
+
70
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
71
+ this software and associated documentation files (the "Software"), to deal in
72
+ the Software without restriction, including without limitation the rights to
73
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
74
+ the Software, and to permit persons to whom the Software is furnished to do so,
75
+ subject to the following conditions:
76
+
77
+ The above copyright notice and this permission notice shall be included in all
78
+ copies or substantial portions of the Software.
79
+
80
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
81
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
82
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
83
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
84
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
85
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.1
2
+ Name: plain
3
+ Version: 0.1.0
4
+ Summary: A web framework for building products with Python.
5
+ Author: Dave Gaeddert
6
+ Author-email: dave.gaeddert@dropseed.dev
7
+ Requires-Python: >=3.11,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Requires-Dist: click (>=8.0.0)
12
+ Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
13
+ Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
14
+ Description-Content-Type: text/markdown
15
+
16
+ <!-- This file is compiled from plain/plain/README.md. Do not edit this file directly. -->
17
+
18
+ # Plain
19
+
20
+ Plain is a web framework for building products with Python.
21
+
22
+ With the core `plain` package you can build an app that:
23
+
24
+ - Matches [URL patterns](https://plainframework.com/docs/plain/plain/urls) to Python [views](https://plainframework.com/docs/plain/plain/views)
25
+ - Handles [HTTP requests and responses](https://plainframework.com/docs/plain/plain/http)
26
+ - Renders [HTML templates](https://plainframework.com/docs/plain/plain/templates) with Jinja
27
+ - Processes user input via [forms](https://plainframework.com/docs/plain/plain/forms)
28
+ - Has a [CLI interface](https://plainframework.com/docs/plain/plain/cli)
29
+ - Serves static [assets](https://plainframework.com/docs/plain/plain/assets) (CSS, JS, images)
30
+ - Can be modified with [middleware](https://plainframework.com/docs/plain/plain/middleware)
31
+ - Integrates first-party and third-party [packages](https://plainframework.com/docs/plain/plain/packages)
32
+ - Has a [preflight check system](https://plainframework.com/docs/plain/plain/preflight)
33
+
34
+ With the official Plain ecosystem packages you can:
35
+
36
+ - Integrate a full-featured [database ORM](https://plainframework.com/docs/plain-models/)
37
+ - Use a built-in [user authentication](https://plainframework.com/docs/plain-auth/) system
38
+ - [Lint and format code](https://plainframework.com/docs/plain-code/)
39
+ - Run a [database-backed cache](https://plainframework.com/docs/plain-cache/)
40
+ - [Send emails](https://plainframework.com/docs/plain-mail/)
41
+ - Streamline [local development](https://plainframework.com/docs/plain-dev/)
42
+ - Manage [feature flags](https://plainframework.com/docs/plain-flags/)
43
+ - Integrate [HTMX](https://plainframework.com/docs/plain-htmx/)
44
+ - Style with [Tailwind CSS](https://plainframework.com/docs/plain-tailwind/)
45
+ - Add [OAuth login](https://plainframework.com/docs/plain-oauth/) and API access
46
+ - Run tests with [pytest](https://plainframework.com/docs/plain-test/)
47
+ - Run a [background job worker](https://plainframework.com/docs/plain-worker/)
48
+ - Build [staff tooling and admin dashboards](https://plainframework.com/docs/plain-staff/)
49
+
50
+ Learn more at [plainframework.com](https://plainframework.com).
51
+