plain 0.53.0__tar.gz → 0.54.0__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 (168) hide show
  1. {plain-0.53.0 → plain-0.54.0}/.gitignore +0 -2
  2. {plain-0.53.0 → plain-0.54.0}/PKG-INFO +4 -1
  3. {plain-0.53.0 → plain-0.54.0}/plain/CHANGELOG.md +11 -0
  4. {plain-0.53.0 → plain-0.54.0}/plain/README.md +1 -0
  5. {plain-0.53.0 → plain-0.54.0}/plain/internal/handlers/base.py +64 -13
  6. plain-0.54.0/plain/templates/core.py +42 -0
  7. {plain-0.53.0 → plain-0.54.0}/plain/views/base.py +26 -3
  8. {plain-0.53.0 → plain-0.54.0}/pyproject.toml +3 -1
  9. plain-0.54.0/tests/.gitignore +1 -0
  10. plain-0.53.0/plain/templates/core.py +0 -24
  11. plain-0.53.0/tests/.gitignore +0 -3
  12. {plain-0.53.0 → plain-0.54.0}/LICENSE +0 -0
  13. {plain-0.53.0 → plain-0.54.0}/README.md +0 -0
  14. {plain-0.53.0 → plain-0.54.0}/plain/__main__.py +0 -0
  15. {plain-0.53.0 → plain-0.54.0}/plain/assets/README.md +0 -0
  16. {plain-0.53.0 → plain-0.54.0}/plain/assets/__init__.py +0 -0
  17. {plain-0.53.0 → plain-0.54.0}/plain/assets/compile.py +0 -0
  18. {plain-0.53.0 → plain-0.54.0}/plain/assets/finders.py +0 -0
  19. {plain-0.53.0 → plain-0.54.0}/plain/assets/fingerprints.py +0 -0
  20. {plain-0.53.0 → plain-0.54.0}/plain/assets/urls.py +0 -0
  21. {plain-0.53.0 → plain-0.54.0}/plain/assets/views.py +0 -0
  22. {plain-0.53.0 → plain-0.54.0}/plain/chores/README.md +0 -0
  23. {plain-0.53.0 → plain-0.54.0}/plain/chores/__init__.py +0 -0
  24. {plain-0.53.0 → plain-0.54.0}/plain/chores/registry.py +0 -0
  25. {plain-0.53.0 → plain-0.54.0}/plain/cli/README.md +0 -0
  26. {plain-0.53.0 → plain-0.54.0}/plain/cli/__init__.py +0 -0
  27. {plain-0.53.0 → plain-0.54.0}/plain/cli/build.py +0 -0
  28. {plain-0.53.0 → plain-0.54.0}/plain/cli/changelog.py +0 -0
  29. {plain-0.53.0 → plain-0.54.0}/plain/cli/chores.py +0 -0
  30. {plain-0.53.0 → plain-0.54.0}/plain/cli/core.py +0 -0
  31. {plain-0.53.0 → plain-0.54.0}/plain/cli/docs.py +0 -0
  32. {plain-0.53.0 → plain-0.54.0}/plain/cli/formatting.py +0 -0
  33. {plain-0.53.0 → plain-0.54.0}/plain/cli/help.py +0 -0
  34. {plain-0.53.0 → plain-0.54.0}/plain/cli/output.py +0 -0
  35. {plain-0.53.0 → plain-0.54.0}/plain/cli/preflight.py +0 -0
  36. {plain-0.53.0 → plain-0.54.0}/plain/cli/print.py +0 -0
  37. {plain-0.53.0 → plain-0.54.0}/plain/cli/registry.py +0 -0
  38. {plain-0.53.0 → plain-0.54.0}/plain/cli/scaffold.py +0 -0
  39. {plain-0.53.0 → plain-0.54.0}/plain/cli/settings.py +0 -0
  40. {plain-0.53.0 → plain-0.54.0}/plain/cli/shell.py +0 -0
  41. {plain-0.53.0 → plain-0.54.0}/plain/cli/startup.py +0 -0
  42. {plain-0.53.0 → plain-0.54.0}/plain/cli/urls.py +0 -0
  43. {plain-0.53.0 → plain-0.54.0}/plain/cli/utils.py +0 -0
  44. {plain-0.53.0 → plain-0.54.0}/plain/csrf/README.md +0 -0
  45. {plain-0.53.0 → plain-0.54.0}/plain/csrf/middleware.py +0 -0
  46. {plain-0.53.0 → plain-0.54.0}/plain/csrf/views.py +0 -0
  47. {plain-0.53.0 → plain-0.54.0}/plain/debug.py +0 -0
  48. {plain-0.53.0 → plain-0.54.0}/plain/exceptions.py +0 -0
  49. {plain-0.53.0 → plain-0.54.0}/plain/forms/README.md +0 -0
  50. {plain-0.53.0 → plain-0.54.0}/plain/forms/__init__.py +0 -0
  51. {plain-0.53.0 → plain-0.54.0}/plain/forms/boundfield.py +0 -0
  52. {plain-0.53.0 → plain-0.54.0}/plain/forms/exceptions.py +0 -0
  53. {plain-0.53.0 → plain-0.54.0}/plain/forms/fields.py +0 -0
  54. {plain-0.53.0 → plain-0.54.0}/plain/forms/forms.py +0 -0
  55. {plain-0.53.0 → plain-0.54.0}/plain/http/README.md +0 -0
  56. {plain-0.53.0 → plain-0.54.0}/plain/http/__init__.py +0 -0
  57. {plain-0.53.0 → plain-0.54.0}/plain/http/cookie.py +0 -0
  58. {plain-0.53.0 → plain-0.54.0}/plain/http/multipartparser.py +0 -0
  59. {plain-0.53.0 → plain-0.54.0}/plain/http/request.py +0 -0
  60. {plain-0.53.0 → plain-0.54.0}/plain/http/response.py +0 -0
  61. {plain-0.53.0 → plain-0.54.0}/plain/internal/__init__.py +0 -0
  62. {plain-0.53.0 → plain-0.54.0}/plain/internal/files/__init__.py +0 -0
  63. {plain-0.53.0 → plain-0.54.0}/plain/internal/files/base.py +0 -0
  64. {plain-0.53.0 → plain-0.54.0}/plain/internal/files/locks.py +0 -0
  65. {plain-0.53.0 → plain-0.54.0}/plain/internal/files/move.py +0 -0
  66. {plain-0.53.0 → plain-0.54.0}/plain/internal/files/temp.py +0 -0
  67. {plain-0.53.0 → plain-0.54.0}/plain/internal/files/uploadedfile.py +0 -0
  68. {plain-0.53.0 → plain-0.54.0}/plain/internal/files/uploadhandler.py +0 -0
  69. {plain-0.53.0 → plain-0.54.0}/plain/internal/files/utils.py +0 -0
  70. {plain-0.53.0 → plain-0.54.0}/plain/internal/handlers/__init__.py +0 -0
  71. {plain-0.53.0 → plain-0.54.0}/plain/internal/handlers/exception.py +0 -0
  72. {plain-0.53.0 → plain-0.54.0}/plain/internal/handlers/wsgi.py +0 -0
  73. {plain-0.53.0 → plain-0.54.0}/plain/internal/middleware/__init__.py +0 -0
  74. {plain-0.53.0 → plain-0.54.0}/plain/internal/middleware/headers.py +0 -0
  75. {plain-0.53.0 → plain-0.54.0}/plain/internal/middleware/https.py +0 -0
  76. {plain-0.53.0 → plain-0.54.0}/plain/internal/middleware/slash.py +0 -0
  77. {plain-0.53.0 → plain-0.54.0}/plain/json.py +0 -0
  78. {plain-0.53.0 → plain-0.54.0}/plain/logs/README.md +0 -0
  79. {plain-0.53.0 → plain-0.54.0}/plain/logs/__init__.py +0 -0
  80. {plain-0.53.0 → plain-0.54.0}/plain/logs/configure.py +0 -0
  81. {plain-0.53.0 → plain-0.54.0}/plain/logs/loggers.py +0 -0
  82. {plain-0.53.0 → plain-0.54.0}/plain/logs/utils.py +0 -0
  83. {plain-0.53.0 → plain-0.54.0}/plain/packages/README.md +0 -0
  84. {plain-0.53.0 → plain-0.54.0}/plain/packages/__init__.py +0 -0
  85. {plain-0.53.0 → plain-0.54.0}/plain/packages/config.py +0 -0
  86. {plain-0.53.0 → plain-0.54.0}/plain/packages/registry.py +0 -0
  87. {plain-0.53.0 → plain-0.54.0}/plain/paginator.py +0 -0
  88. {plain-0.53.0 → plain-0.54.0}/plain/preflight/README.md +0 -0
  89. {plain-0.53.0 → plain-0.54.0}/plain/preflight/__init__.py +0 -0
  90. {plain-0.53.0 → plain-0.54.0}/plain/preflight/files.py +0 -0
  91. {plain-0.53.0 → plain-0.54.0}/plain/preflight/messages.py +0 -0
  92. {plain-0.53.0 → plain-0.54.0}/plain/preflight/registry.py +0 -0
  93. {plain-0.53.0 → plain-0.54.0}/plain/preflight/security.py +0 -0
  94. {plain-0.53.0 → plain-0.54.0}/plain/preflight/urls.py +0 -0
  95. {plain-0.53.0 → plain-0.54.0}/plain/runtime/README.md +0 -0
  96. {plain-0.53.0 → plain-0.54.0}/plain/runtime/__init__.py +0 -0
  97. {plain-0.53.0 → plain-0.54.0}/plain/runtime/global_settings.py +0 -0
  98. {plain-0.53.0 → plain-0.54.0}/plain/runtime/user_settings.py +0 -0
  99. {plain-0.53.0 → plain-0.54.0}/plain/signals/README.md +0 -0
  100. {plain-0.53.0 → plain-0.54.0}/plain/signals/__init__.py +0 -0
  101. {plain-0.53.0 → plain-0.54.0}/plain/signals/dispatch/__init__.py +0 -0
  102. {plain-0.53.0 → plain-0.54.0}/plain/signals/dispatch/dispatcher.py +0 -0
  103. {plain-0.53.0 → plain-0.54.0}/plain/signals/dispatch/license.txt +0 -0
  104. {plain-0.53.0 → plain-0.54.0}/plain/signing.py +0 -0
  105. {plain-0.53.0 → plain-0.54.0}/plain/templates/README.md +0 -0
  106. {plain-0.53.0 → plain-0.54.0}/plain/templates/__init__.py +0 -0
  107. {plain-0.53.0 → plain-0.54.0}/plain/templates/jinja/__init__.py +0 -0
  108. {plain-0.53.0 → plain-0.54.0}/plain/templates/jinja/environments.py +0 -0
  109. {plain-0.53.0 → plain-0.54.0}/plain/templates/jinja/extensions.py +0 -0
  110. {plain-0.53.0 → plain-0.54.0}/plain/templates/jinja/filters.py +0 -0
  111. {plain-0.53.0 → plain-0.54.0}/plain/templates/jinja/globals.py +0 -0
  112. {plain-0.53.0 → plain-0.54.0}/plain/test/README.md +0 -0
  113. {plain-0.53.0 → plain-0.54.0}/plain/test/__init__.py +0 -0
  114. {plain-0.53.0 → plain-0.54.0}/plain/test/client.py +0 -0
  115. {plain-0.53.0 → plain-0.54.0}/plain/test/encoding.py +0 -0
  116. {plain-0.53.0 → plain-0.54.0}/plain/test/exceptions.py +0 -0
  117. {plain-0.53.0 → plain-0.54.0}/plain/urls/README.md +0 -0
  118. {plain-0.53.0 → plain-0.54.0}/plain/urls/__init__.py +0 -0
  119. {plain-0.53.0 → plain-0.54.0}/plain/urls/converters.py +0 -0
  120. {plain-0.53.0 → plain-0.54.0}/plain/urls/exceptions.py +0 -0
  121. {plain-0.53.0 → plain-0.54.0}/plain/urls/patterns.py +0 -0
  122. {plain-0.53.0 → plain-0.54.0}/plain/urls/resolvers.py +0 -0
  123. {plain-0.53.0 → plain-0.54.0}/plain/urls/routers.py +0 -0
  124. {plain-0.53.0 → plain-0.54.0}/plain/urls/utils.py +0 -0
  125. {plain-0.53.0 → plain-0.54.0}/plain/utils/README.md +0 -0
  126. {plain-0.53.0 → plain-0.54.0}/plain/utils/__init__.py +0 -0
  127. {plain-0.53.0 → plain-0.54.0}/plain/utils/cache.py +0 -0
  128. {plain-0.53.0 → plain-0.54.0}/plain/utils/crypto.py +0 -0
  129. {plain-0.53.0 → plain-0.54.0}/plain/utils/datastructures.py +0 -0
  130. {plain-0.53.0 → plain-0.54.0}/plain/utils/dateparse.py +0 -0
  131. {plain-0.53.0 → plain-0.54.0}/plain/utils/deconstruct.py +0 -0
  132. {plain-0.53.0 → plain-0.54.0}/plain/utils/decorators.py +0 -0
  133. {plain-0.53.0 → plain-0.54.0}/plain/utils/duration.py +0 -0
  134. {plain-0.53.0 → plain-0.54.0}/plain/utils/encoding.py +0 -0
  135. {plain-0.53.0 → plain-0.54.0}/plain/utils/functional.py +0 -0
  136. {plain-0.53.0 → plain-0.54.0}/plain/utils/hashable.py +0 -0
  137. {plain-0.53.0 → plain-0.54.0}/plain/utils/html.py +0 -0
  138. {plain-0.53.0 → plain-0.54.0}/plain/utils/http.py +0 -0
  139. {plain-0.53.0 → plain-0.54.0}/plain/utils/inspect.py +0 -0
  140. {plain-0.53.0 → plain-0.54.0}/plain/utils/ipv6.py +0 -0
  141. {plain-0.53.0 → plain-0.54.0}/plain/utils/itercompat.py +0 -0
  142. {plain-0.53.0 → plain-0.54.0}/plain/utils/module_loading.py +0 -0
  143. {plain-0.53.0 → plain-0.54.0}/plain/utils/regex_helper.py +0 -0
  144. {plain-0.53.0 → plain-0.54.0}/plain/utils/safestring.py +0 -0
  145. {plain-0.53.0 → plain-0.54.0}/plain/utils/text.py +0 -0
  146. {plain-0.53.0 → plain-0.54.0}/plain/utils/timesince.py +0 -0
  147. {plain-0.53.0 → plain-0.54.0}/plain/utils/timezone.py +0 -0
  148. {plain-0.53.0 → plain-0.54.0}/plain/utils/tree.py +0 -0
  149. {plain-0.53.0 → plain-0.54.0}/plain/validators.py +0 -0
  150. {plain-0.53.0 → plain-0.54.0}/plain/views/README.md +0 -0
  151. {plain-0.53.0 → plain-0.54.0}/plain/views/__init__.py +0 -0
  152. {plain-0.53.0 → plain-0.54.0}/plain/views/csrf.py +0 -0
  153. {plain-0.53.0 → plain-0.54.0}/plain/views/errors.py +0 -0
  154. {plain-0.53.0 → plain-0.54.0}/plain/views/exceptions.py +0 -0
  155. {plain-0.53.0 → plain-0.54.0}/plain/views/forms.py +0 -0
  156. {plain-0.53.0 → plain-0.54.0}/plain/views/objects.py +0 -0
  157. {plain-0.53.0 → plain-0.54.0}/plain/views/redirect.py +0 -0
  158. {plain-0.53.0 → plain-0.54.0}/plain/views/templates.py +0 -0
  159. {plain-0.53.0 → plain-0.54.0}/plain/wsgi.py +0 -0
  160. {plain-0.53.0 → plain-0.54.0}/tests/app/.gitignore +0 -0
  161. {plain-0.53.0 → plain-0.54.0}/tests/app/settings.py +0 -0
  162. {plain-0.53.0 → plain-0.54.0}/tests/app/test/__init__.py +0 -0
  163. {plain-0.53.0 → plain-0.54.0}/tests/app/test/default_settings.py +0 -0
  164. {plain-0.53.0 → plain-0.54.0}/tests/app/urls.py +0 -0
  165. {plain-0.53.0 → plain-0.54.0}/tests/conftest.py +0 -0
  166. {plain-0.53.0 → plain-0.54.0}/tests/test_cli.py +0 -0
  167. {plain-0.53.0 → plain-0.54.0}/tests/test_runtime.py +0 -0
  168. {plain-0.53.0 → plain-0.54.0}/tests/test_wsgi.py +0 -0
@@ -4,7 +4,6 @@
4
4
  *.py[co]
5
5
  __pycache__
6
6
  *.DS_Store
7
- .coverage
8
7
 
9
8
  # Test apps
10
9
  plain*/tests/.plain
@@ -17,5 +16,4 @@ plain*/tests/.plain
17
16
  # Plain temp dirs
18
17
  .plain
19
18
 
20
- coverage.xml
21
19
  .vscode
@@ -1,12 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.53.0
3
+ Version: 0.54.0
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
7
7
  Requires-Python: >=3.11
8
8
  Requires-Dist: click>=8.0.0
9
9
  Requires-Dist: jinja2>=3.1.2
10
+ Requires-Dist: opentelemetry-api>=1.34.1
11
+ Requires-Dist: opentelemetry-semantic-conventions>=0.55b1
10
12
  Description-Content-Type: text/markdown
11
13
 
12
14
  # Plain
@@ -59,6 +61,7 @@ The `plain` package includes everything you need to start handling web requests
59
61
  - [plain.support](/plain-support/plain/support/README.md) - Customer support forms.
60
62
  - [plain.redirection](/plain-redirection/plain/redirection/README.md) - Redirects managed in the database.
61
63
  - [plain.pageviews](/plain-pageviews/plain/pageviews/README.md) - Basic self-hosted page view tracking and reporting.
64
+ - [plain.observer](/plain-observer/plain/observer/README.md) - On-page telemetry reporting.
62
65
 
63
66
  ## Dev Packages
64
67
 
@@ -1,5 +1,16 @@
1
1
  # plain changelog
2
2
 
3
+ ## [0.54.0](https://github.com/dropseed/plain/releases/plain@0.54.0) (2025-07-18)
4
+
5
+ ### What's changed
6
+
7
+ - Added OpenTelemetry instrumentation for HTTP requests, views, and template rendering ([b0224d0418](https://github.com/dropseed/plain/commit/b0224d0418))
8
+ - Added `plain-observer` package reference to plain README ([f29ff4dafe](https://github.com/dropseed/plain/commit/f29ff4dafe))
9
+
10
+ ### Upgrade instructions
11
+
12
+ - No changes required
13
+
3
14
  ## [0.53.0](https://github.com/dropseed/plain/releases/plain@0.53.0) (2025-07-18)
4
15
 
5
16
  ### What's changed
@@ -48,6 +48,7 @@ The `plain` package includes everything you need to start handling web requests
48
48
  - [plain.support](/plain-support/plain/support/README.md) - Customer support forms.
49
49
  - [plain.redirection](/plain-redirection/plain/redirection/README.md) - Redirects managed in the database.
50
50
  - [plain.pageviews](/plain-pageviews/plain/pageviews/README.md) - Basic self-hosted page view tracking and reporting.
51
+ - [plain.observer](/plain-observer/plain/observer/README.md) - On-page telemetry reporting.
51
52
 
52
53
  ## Dev Packages
53
54
 
@@ -1,6 +1,9 @@
1
1
  import logging
2
2
  import types
3
3
 
4
+ from opentelemetry import baggage, trace
5
+ from opentelemetry.semconv.attributes import http_attributes, url_attributes
6
+
4
7
  from plain.exceptions import ImproperlyConfigured
5
8
  from plain.logs import log_response
6
9
  from plain.runtime import settings
@@ -26,6 +29,9 @@ BUILTIN_AFTER_MIDDLEWARE = [
26
29
  ]
27
30
 
28
31
 
32
+ tracer = trace.get_tracer("plain")
33
+
34
+
29
35
  class BaseHandler:
30
36
  _middleware_chain = None
31
37
 
@@ -35,8 +41,7 @@ class BaseHandler:
35
41
 
36
42
  Must be called after the environment is fixed (see __call__ in subclasses).
37
43
  """
38
- get_response = self._get_response
39
- handler = convert_exception_to_response(get_response)
44
+ handler = convert_exception_to_response(self._get_response)
40
45
 
41
46
  middlewares = reversed(
42
47
  BUILTIN_BEFORE_MIDDLEWARE + settings.MIDDLEWARE + BUILTIN_AFTER_MIDDLEWARE
@@ -59,18 +64,55 @@ class BaseHandler:
59
64
 
60
65
  def get_response(self, request):
61
66
  """Return a Response object for the given HttpRequest."""
62
- # Setup default url resolver for this thread
63
- response = self._middleware_chain(request)
64
- response._resource_closers.append(request.close)
65
- if response.status_code >= 400:
66
- log_response(
67
- "%s: %s",
68
- response.reason_phrase,
69
- request.path,
70
- response=response,
71
- request=request,
67
+
68
+ span_attributes = {
69
+ "plain.request.id": request.unique_id,
70
+ http_attributes.HTTP_REQUEST_METHOD: request.method,
71
+ url_attributes.URL_PATH: request.path_info,
72
+ url_attributes.URL_SCHEME: request.scheme,
73
+ }
74
+
75
+ # Add full URL if we can build it (requires proper WSGI environment)
76
+ try:
77
+ span_attributes[url_attributes.URL_FULL] = request.build_absolute_uri()
78
+ except KeyError:
79
+ # Missing required WSGI environment variables (e.g. in tests)
80
+ pass
81
+
82
+ # Add query string if present
83
+ if query_string := request.meta.get("QUERY_STRING"):
84
+ span_attributes[url_attributes.URL_QUERY] = query_string
85
+
86
+ span_context = baggage.set_baggage("http.request.cookies", request.cookies)
87
+
88
+ with tracer.start_as_current_span(
89
+ f"{request.method} {request.path_info}",
90
+ context=span_context,
91
+ attributes=span_attributes,
92
+ kind=trace.SpanKind.SERVER,
93
+ ) as span:
94
+ response = self._middleware_chain(request)
95
+ response._resource_closers.append(request.close)
96
+
97
+ span.set_attribute(
98
+ http_attributes.HTTP_RESPONSE_STATUS_CODE, response.status_code
99
+ )
100
+
101
+ span.set_status(
102
+ trace.StatusCode.OK
103
+ if response.status_code < 400
104
+ else trace.StatusCode.ERROR
72
105
  )
73
- return response
106
+
107
+ if response.status_code >= 400:
108
+ log_response(
109
+ "%s: %s",
110
+ response.reason_phrase,
111
+ request.path,
112
+ response=response,
113
+ request=request,
114
+ )
115
+ return response
74
116
 
75
117
  def _get_response(self, request):
76
118
  """
@@ -94,9 +136,18 @@ class BaseHandler:
94
136
  Retrieve/set the urlrouter for the request. Return the view resolved,
95
137
  with its args and kwargs.
96
138
  """
139
+
97
140
  resolver = get_resolver()
98
141
  # Resolve the view, and assign the match object back to the request.
99
142
  resolver_match = resolver.resolve(request.path_info)
143
+
144
+ span = trace.get_current_span()
145
+ span.set_attribute(http_attributes.HTTP_ROUTE, resolver_match.route)
146
+
147
+ # Route makes a better name
148
+ if resolver_match.route:
149
+ span.update_name(f"{request.method} {resolver_match.route}")
150
+
100
151
  request.resolver_match = resolver_match
101
152
  return resolver_match
102
153
 
@@ -0,0 +1,42 @@
1
+ import jinja2
2
+ from opentelemetry import trace
3
+ from opentelemetry.semconv._incubating.attributes.code_attributes import (
4
+ CODE_FUNCTION_NAME,
5
+ CODE_NAMESPACE,
6
+ )
7
+
8
+ from .jinja import environment
9
+
10
+ tracer = trace.get_tracer("plain")
11
+
12
+
13
+ class TemplateFileMissing(Exception):
14
+ def __str__(self) -> str:
15
+ if self.args:
16
+ return f"Template file {self.args[0]} not found"
17
+ else:
18
+ return "Template file not found"
19
+
20
+
21
+ class Template:
22
+ def __init__(self, filename: str) -> None:
23
+ self.filename = filename
24
+
25
+ try:
26
+ self._jinja_template = environment.get_template(filename)
27
+ except jinja2.TemplateNotFound:
28
+ raise TemplateFileMissing(filename)
29
+
30
+ def render(self, context: dict) -> str:
31
+ with tracer.start_as_current_span(
32
+ f"render {self.filename}",
33
+ kind=trace.SpanKind.INTERNAL,
34
+ attributes={
35
+ CODE_FUNCTION_NAME: "render",
36
+ CODE_NAMESPACE: f"{self.__class__.__module__}.{self.__class__.__qualname__}",
37
+ "template.filename": self.filename,
38
+ "template.engine": "jinja2",
39
+ },
40
+ ):
41
+ result = self._jinja_template.render(context)
42
+ return result
@@ -1,6 +1,12 @@
1
1
  import logging
2
2
  from http import HTTPMethod
3
3
 
4
+ from opentelemetry import trace
5
+ from opentelemetry.semconv._incubating.attributes.code_attributes import (
6
+ CODE_FUNCTION_NAME,
7
+ CODE_NAMESPACE,
8
+ )
9
+
4
10
  from plain.http import (
5
11
  HttpRequest,
6
12
  JsonResponse,
@@ -16,6 +22,9 @@ from .exceptions import ResponseException
16
22
  logger = logging.getLogger("plain.request")
17
23
 
18
24
 
25
+ tracer = trace.get_tracer("plain")
26
+
27
+
19
28
  class View:
20
29
  request: HttpRequest
21
30
  url_args: tuple
@@ -35,9 +44,23 @@ class View:
35
44
  @classonlymethod
36
45
  def as_view(cls, *init_args, **init_kwargs):
37
46
  def view(request, *url_args, **url_kwargs):
38
- v = cls(*init_args, **init_kwargs)
39
- v.setup(request, *url_args, **url_kwargs)
40
- return v.get_response()
47
+ with tracer.start_as_current_span(
48
+ f"{cls.__name__}",
49
+ kind=trace.SpanKind.INTERNAL,
50
+ attributes={
51
+ CODE_FUNCTION_NAME: "as_view",
52
+ CODE_NAMESPACE: f"{cls.__module__}.{cls.__qualname__}",
53
+ },
54
+ ) as span:
55
+ v = cls(*init_args, **init_kwargs)
56
+ v.setup(request, *url_args, **url_kwargs)
57
+ response = v.get_response()
58
+ span.set_status(
59
+ trace.StatusCode.OK
60
+ if response.status_code < 400
61
+ else trace.StatusCode.ERROR
62
+ )
63
+ return response
41
64
 
42
65
  view.view_class = cls
43
66
 
@@ -1,12 +1,14 @@
1
1
  [project]
2
2
  name = "plain"
3
- version = "0.53.0"
3
+ version = "0.54.0"
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"
7
7
  dependencies = [
8
8
  "jinja2>=3.1.2",
9
9
  "click>=8.0.0",
10
+ "opentelemetry-api>=1.34.1",
11
+ "opentelemetry-semantic-conventions>=0.55b1",
10
12
  ]
11
13
  requires-python = ">=3.11"
12
14
 
@@ -0,0 +1 @@
1
+ .plain
@@ -1,24 +0,0 @@
1
- import jinja2
2
-
3
- from .jinja import environment
4
-
5
-
6
- class TemplateFileMissing(Exception):
7
- def __str__(self) -> str:
8
- if self.args:
9
- return f"Template file {self.args[0]} not found"
10
- else:
11
- return "Template file not found"
12
-
13
-
14
- class Template:
15
- def __init__(self, filename: str) -> None:
16
- self.filename = filename
17
-
18
- try:
19
- self._jinja_template = environment.get_template(filename)
20
- except jinja2.TemplateNotFound:
21
- raise TemplateFileMissing(filename)
22
-
23
- def render(self, context: dict) -> str:
24
- return self._jinja_template.render(context)
@@ -1,3 +0,0 @@
1
- /.coverage
2
- /htmlcov
3
- .plain
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
File without changes
File without changes
File without changes