pyview-web 0.3.0__py3-none-any.whl → 0.8.0a2__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 (78) hide show
  1. pyview/__init__.py +16 -6
  2. pyview/assets/js/app.js +1 -0
  3. pyview/assets/js/uploaders.js +221 -0
  4. pyview/assets/package-lock.json +16 -14
  5. pyview/assets/package.json +2 -2
  6. pyview/async_stream_runner.py +2 -1
  7. pyview/auth/__init__.py +3 -1
  8. pyview/auth/provider.py +6 -6
  9. pyview/auth/required.py +7 -10
  10. pyview/binding/__init__.py +47 -0
  11. pyview/binding/binder.py +134 -0
  12. pyview/binding/context.py +33 -0
  13. pyview/binding/converters.py +191 -0
  14. pyview/binding/helpers.py +78 -0
  15. pyview/binding/injectables.py +119 -0
  16. pyview/binding/params.py +105 -0
  17. pyview/binding/result.py +32 -0
  18. pyview/changesets/__init__.py +2 -0
  19. pyview/changesets/changesets.py +8 -3
  20. pyview/cli/commands/create_view.py +4 -3
  21. pyview/cli/main.py +1 -1
  22. pyview/components/__init__.py +72 -0
  23. pyview/components/base.py +212 -0
  24. pyview/components/lifecycle.py +85 -0
  25. pyview/components/manager.py +366 -0
  26. pyview/components/renderer.py +14 -0
  27. pyview/components/slots.py +73 -0
  28. pyview/csrf.py +4 -2
  29. pyview/events/AutoEventDispatch.py +98 -0
  30. pyview/events/BaseEventHandler.py +51 -8
  31. pyview/events/__init__.py +2 -1
  32. pyview/instrumentation/__init__.py +3 -3
  33. pyview/instrumentation/interfaces.py +57 -33
  34. pyview/instrumentation/noop.py +21 -18
  35. pyview/js.py +20 -23
  36. pyview/live_routes.py +5 -3
  37. pyview/live_socket.py +167 -44
  38. pyview/live_view.py +24 -12
  39. pyview/meta.py +14 -2
  40. pyview/phx_message.py +7 -8
  41. pyview/playground/__init__.py +10 -0
  42. pyview/playground/builder.py +118 -0
  43. pyview/playground/favicon.py +39 -0
  44. pyview/pyview.py +54 -20
  45. pyview/session.py +2 -0
  46. pyview/static/assets/app.js +2088 -806
  47. pyview/static/assets/uploaders.js +221 -0
  48. pyview/stream.py +308 -0
  49. pyview/template/__init__.py +11 -1
  50. pyview/template/live_template.py +12 -8
  51. pyview/template/live_view_template.py +338 -0
  52. pyview/template/render_diff.py +33 -7
  53. pyview/template/root_template.py +21 -9
  54. pyview/template/serializer.py +2 -5
  55. pyview/template/template_view.py +170 -0
  56. pyview/template/utils.py +3 -2
  57. pyview/uploads.py +344 -55
  58. pyview/vendor/flet/pubsub/__init__.py +3 -1
  59. pyview/vendor/flet/pubsub/pub_sub.py +10 -18
  60. pyview/vendor/ibis/__init__.py +3 -7
  61. pyview/vendor/ibis/compiler.py +25 -32
  62. pyview/vendor/ibis/context.py +13 -15
  63. pyview/vendor/ibis/errors.py +0 -6
  64. pyview/vendor/ibis/filters.py +70 -76
  65. pyview/vendor/ibis/loaders.py +6 -7
  66. pyview/vendor/ibis/nodes.py +40 -42
  67. pyview/vendor/ibis/template.py +4 -5
  68. pyview/vendor/ibis/tree.py +62 -3
  69. pyview/vendor/ibis/utils.py +14 -15
  70. pyview/ws_handler.py +116 -86
  71. {pyview_web-0.3.0.dist-info → pyview_web-0.8.0a2.dist-info}/METADATA +39 -33
  72. pyview_web-0.8.0a2.dist-info/RECORD +80 -0
  73. pyview_web-0.8.0a2.dist-info/WHEEL +4 -0
  74. pyview_web-0.8.0a2.dist-info/entry_points.txt +3 -0
  75. pyview_web-0.3.0.dist-info/LICENSE +0 -21
  76. pyview_web-0.3.0.dist-info/RECORD +0 -58
  77. pyview_web-0.3.0.dist-info/WHEEL +0 -4
  78. pyview_web-0.3.0.dist-info/entry_points.txt +0 -3
@@ -0,0 +1,73 @@
1
+ """
2
+ Slots support for LiveComponents.
3
+
4
+ Slots allow parent templates to pass content into component slots,
5
+ similar to React's children/slots or Phoenix LiveView's slots.
6
+
7
+ Usage:
8
+ from pyview.components import live_component, slots
9
+
10
+ # Default slot only
11
+ live_component(Card, id="card-1", slots=slots(
12
+ t"<p>This is the card body content</p>"
13
+ ))
14
+
15
+ # Named slots
16
+ live_component(Card, id="card-1", slots=slots(
17
+ t"<p>Default body content</p>",
18
+ header=t"<h2>Card Title</h2>",
19
+ footer=t"<button>Submit</button>"
20
+ ))
21
+
22
+ # In the component template
23
+ class Card(LiveComponent[CardContext]):
24
+ def template(self, assigns: CardContext, meta: ComponentMeta):
25
+ return t'''
26
+ <div class="card">
27
+ <header>{meta.slots['header']}</header>
28
+ <main>{meta.slots['default']}</main>
29
+ <footer>{meta.slots['footer']}</footer>
30
+ </div>
31
+ '''
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from typing import TYPE_CHECKING, Any
37
+
38
+ if TYPE_CHECKING:
39
+ from string.templatelib import Template # type: ignore[import-not-found]
40
+ from typing import TypeAlias
41
+
42
+ Slots: TypeAlias = dict[str, Template]
43
+ else:
44
+ # Runtime: use Any to avoid import errors on Python < 3.14
45
+ Slots = dict[str, Any]
46
+
47
+
48
+ def slots(__default: Template | None = None, **named: Template) -> Slots:
49
+ """
50
+ Create a slots dictionary for component children.
51
+
52
+ Args:
53
+ __default: The default/unnamed slot content (positional argument)
54
+ **named: Named slots (header, footer, etc.)
55
+
56
+ Returns:
57
+ Dictionary mapping slot names to template content
58
+
59
+ Examples:
60
+ # Default slot only
61
+ slots(t"<p>Body content</p>")
62
+
63
+ # Named slots only
64
+ slots(header=t"<h2>Title</h2>", footer=t"<button>OK</button>")
65
+
66
+ # Both default and named slots
67
+ slots(t"<p>Body</p>", header=t"<h2>Title</h2>")
68
+ """
69
+ result: Slots = {}
70
+ if __default is not None:
71
+ result["default"] = __default
72
+ result.update(named)
73
+ return result
pyview/csrf.py CHANGED
@@ -1,7 +1,9 @@
1
- from itsdangerous import BadData, SignatureExpired, URLSafeTimedSerializer
2
- from typing import Optional
3
1
  import hmac
4
2
  import logging
3
+ from typing import Optional
4
+
5
+ from itsdangerous import BadData, SignatureExpired, URLSafeTimedSerializer
6
+
5
7
  from pyview.secret import get_secret
6
8
 
7
9
  logger = logging.getLogger(__name__)
@@ -0,0 +1,98 @@
1
+ from typing import Callable
2
+
3
+ from .BaseEventHandler import BaseEventHandler
4
+
5
+
6
+ class BoundEventMethod:
7
+ """
8
+ A bound method wrapper that can be stringified to its event name.
9
+
10
+ This allows methods to be referenced in templates like {self.increment}
11
+ and have them automatically convert to their event name string.
12
+ """
13
+
14
+ def __init__(self, instance, func: Callable, event_name: str):
15
+ self.instance = instance
16
+ self.func = func
17
+ self.event_name = event_name
18
+
19
+ def __str__(self) -> str:
20
+ """Return the event name when converted to string (for template interpolation)."""
21
+ return self.event_name
22
+
23
+ async def __call__(self, *args, **kwargs):
24
+ """Still callable as a normal async method."""
25
+ return await self.func(self.instance, *args, **kwargs)
26
+
27
+
28
+ class EventMethodDescriptor:
29
+ """
30
+ Descriptor that wraps event handler methods.
31
+
32
+ When accessed, returns a BoundEventMethod that:
33
+ 1. Stringifies to the event name (for templates)
34
+ 2. Remains callable (for direct invocation)
35
+ """
36
+
37
+ def __init__(self, func: Callable, event_name: str):
38
+ self.func = func
39
+ self.event_name = event_name
40
+ self.__name__ = func.__name__
41
+ self.__doc__ = func.__doc__
42
+
43
+ def __get__(self, obj, objtype=None):
44
+ if obj is None:
45
+ return self
46
+ return BoundEventMethod(obj, self.func, self.event_name)
47
+
48
+ def __set_name__(self, owner, name):
49
+ """Called when the descriptor is assigned to a class attribute."""
50
+ if not self.event_name:
51
+ self.event_name = name
52
+
53
+
54
+ class AutoEventDispatch(BaseEventHandler):
55
+ """
56
+ Base class that enables automatic event dispatch from function references.
57
+
58
+ Methods decorated with @event (with or without explicit names) can be
59
+ referenced directly in templates, automatically converting to their event name.
60
+
61
+ Inherits from BaseEventHandler and extends it by wrapping decorated methods
62
+ with descriptors for template stringification.
63
+
64
+ Usage:
65
+ class MyView(AutoEventDispatch, TemplateView, LiveView):
66
+ @event # or @event()
67
+ async def increment(self, event, payload, socket):
68
+ socket.context["count"] += 1
69
+
70
+ @event("custom-name")
71
+ async def some_handler(self, event, payload, socket):
72
+ pass
73
+
74
+ def template(self, assigns, meta):
75
+ return t'''
76
+ <button phx-click={self.increment}>+</button>
77
+ <button phx-click={self.some_handler}>Custom</button>
78
+ '''
79
+ """
80
+
81
+ def __init_subclass__(cls, **kwargs):
82
+ super().__init_subclass__(**kwargs)
83
+
84
+ # BaseEventHandler already populated _event_handlers with raw functions
85
+ # Now we wrap them with descriptors for template stringification
86
+ for event_name, handler in list(cls._event_handlers.items()):
87
+ # Find which attribute this handler came from
88
+ for attr_name in dir(cls):
89
+ if not attr_name.startswith("_"):
90
+ attr = getattr(cls, attr_name, None)
91
+ if attr is handler or (
92
+ isinstance(attr, EventMethodDescriptor) and attr.func is handler
93
+ ):
94
+ # Wrap with descriptor if not already wrapped
95
+ if not isinstance(attr, EventMethodDescriptor):
96
+ descriptor = EventMethodDescriptor(handler, event_name)
97
+ setattr(cls, attr_name, descriptor)
98
+ break
@@ -1,15 +1,39 @@
1
- from typing import Callable, TYPE_CHECKING
2
1
  import logging
2
+ from typing import TYPE_CHECKING, Any, Callable
3
+
4
+ from pyview.binding import BindContext, Binder, Params
3
5
 
4
6
  if TYPE_CHECKING:
5
7
  from pyview.live_view import InfoEvent
6
8
 
9
+ logger = logging.getLogger(__name__)
10
+
7
11
 
8
12
  def event(*event_names):
9
- """Decorator that marks methods as event handlers."""
13
+ """
14
+ Decorator that marks methods as event handlers.
15
+
16
+ Can be used with or without explicit event names:
17
+ @event # Uses method name as event name
18
+ @event() # Uses method name as event name
19
+ @event("custom-name") # Uses "custom-name" as event name
20
+ @event("name1", "name2") # Handles multiple event names
21
+
22
+ When used with AutoEventDispatch, methods without explicit names can be
23
+ referenced directly in templates: phx-click={self.increment}
24
+ """
25
+
26
+ def decorator(func: Callable) -> Callable:
27
+ # If no event names provided, use the function name
28
+ names = event_names if event_names else (func.__name__,)
29
+ func_any: Any = func
30
+ func_any._event_names = names
31
+ return func
10
32
 
11
- def decorator(func):
12
- func._event_names = event_names
33
+ # Handle @event without parentheses (decorator applied directly to function)
34
+ if len(event_names) == 1 and callable(event_names[0]):
35
+ func: Any = event_names[0]
36
+ func._event_names = (func.__name__,)
13
37
  return func
14
38
 
15
39
  return decorator
@@ -50,10 +74,29 @@ class BaseEventHandler:
50
74
  async def handle_event(self, event: str, payload: dict, socket):
51
75
  handler = self._event_handlers.get(event)
52
76
 
53
- if handler:
54
- return await handler(self, event, payload, socket)
55
- else:
56
- logging.warning(f"Unhandled event: {event} {payload}")
77
+ if not handler:
78
+ logger.warning(f"Unhandled event: {event} {payload}")
79
+ return
80
+
81
+ # Create bind context for event handling
82
+ ctx = BindContext(
83
+ params=Params({}),
84
+ payload=payload,
85
+ url=None,
86
+ socket=socket,
87
+ event=event,
88
+ )
89
+
90
+ # Bind parameters and invoke handler
91
+ binder = Binder()
92
+ result = binder.bind(handler, ctx)
93
+
94
+ if not result.success:
95
+ for err in result.errors:
96
+ logger.warning(f"Event binding error for '{event}': {err}")
97
+ raise ValueError(f"Event binding failed for '{event}': {result.errors}")
98
+
99
+ return await handler(self, **result.bound_args)
57
100
 
58
101
  async def handle_info(self, event: "InfoEvent", socket):
59
102
  handler = self._info_handlers.get(event.name)
pyview/events/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
+ from .AutoEventDispatch import AutoEventDispatch
1
2
  from .BaseEventHandler import BaseEventHandler, event, info
2
3
  from .info_event import InfoEvent
3
4
 
4
- __all__ = ["BaseEventHandler", "event", "info", "InfoEvent"]
5
+ __all__ = ["AutoEventDispatch", "BaseEventHandler", "event", "info", "InfoEvent"]
@@ -1,12 +1,12 @@
1
1
  """PyView instrumentation interfaces and implementations."""
2
2
 
3
3
  from .interfaces import (
4
- InstrumentationProvider,
5
4
  Counter,
6
- UpDownCounter,
7
5
  Gauge,
8
6
  Histogram,
7
+ InstrumentationProvider,
9
8
  Span,
9
+ UpDownCounter,
10
10
  )
11
11
  from .noop import NoOpInstrumentation
12
12
 
@@ -18,4 +18,4 @@ __all__ = [
18
18
  "Histogram",
19
19
  "Span",
20
20
  "NoOpInstrumentation",
21
- ]
21
+ ]
@@ -1,16 +1,16 @@
1
1
  """Abstract base classes for PyView instrumentation."""
2
2
 
3
- from abc import ABC, abstractmethod
4
- from typing import Any, Optional, Callable, Union
5
- from contextlib import contextmanager
3
+ import asyncio
6
4
  import functools
7
5
  import time
8
- import asyncio
6
+ from abc import ABC, abstractmethod
7
+ from contextlib import contextmanager
8
+ from typing import Any, Callable, Optional
9
9
 
10
10
 
11
11
  class Counter(ABC):
12
12
  """A monotonically increasing counter metric."""
13
-
13
+
14
14
  @abstractmethod
15
15
  def add(self, value: float = 1, attributes: Optional[dict[str, Any]] = None) -> None:
16
16
  """Add to the counter value."""
@@ -19,7 +19,7 @@ class Counter(ABC):
19
19
 
20
20
  class UpDownCounter(ABC):
21
21
  """A counter that can increase or decrease."""
22
-
22
+
23
23
  @abstractmethod
24
24
  def add(self, value: float, attributes: Optional[dict[str, Any]] = None) -> None:
25
25
  """Add to the counter value (can be negative)."""
@@ -28,7 +28,7 @@ class UpDownCounter(ABC):
28
28
 
29
29
  class Gauge(ABC):
30
30
  """A point-in-time value metric."""
31
-
31
+
32
32
  @abstractmethod
33
33
  def set(self, value: float, attributes: Optional[dict[str, Any]] = None) -> None:
34
34
  """Set the gauge to a specific value."""
@@ -37,7 +37,7 @@ class Gauge(ABC):
37
37
 
38
38
  class Histogram(ABC):
39
39
  """A metric that tracks the distribution of values."""
40
-
40
+
41
41
  @abstractmethod
42
42
  def record(self, value: float, attributes: Optional[dict[str, Any]] = None) -> None:
43
43
  """Record a value in the histogram."""
@@ -46,17 +46,17 @@ class Histogram(ABC):
46
46
 
47
47
  class Span(ABC):
48
48
  """A span representing a unit of work."""
49
-
49
+
50
50
  @abstractmethod
51
51
  def set_attribute(self, key: str, value: Any) -> None:
52
52
  """Set an attribute on the span."""
53
53
  pass
54
-
54
+
55
55
  @abstractmethod
56
56
  def add_event(self, name: str, attributes: Optional[dict[str, Any]] = None) -> None:
57
57
  """Add an event to the span."""
58
58
  pass
59
-
59
+
60
60
  @abstractmethod
61
61
  def end(self, status: str = "ok") -> None:
62
62
  """End the span with a status."""
@@ -65,55 +65,65 @@ class Span(ABC):
65
65
 
66
66
  class InstrumentationProvider(ABC):
67
67
  """Abstract base class for instrumentation providers."""
68
-
68
+
69
69
  # Create instruments (efficient, reusable)
70
70
  @abstractmethod
71
71
  def create_counter(self, name: str, description: str = "", unit: str = "") -> Counter:
72
72
  """Create a counter instrument."""
73
73
  pass
74
-
74
+
75
75
  @abstractmethod
76
- def create_updown_counter(self, name: str, description: str = "", unit: str = "") -> UpDownCounter:
76
+ def create_updown_counter(
77
+ self, name: str, description: str = "", unit: str = ""
78
+ ) -> UpDownCounter:
77
79
  """Create an up/down counter instrument."""
78
80
  pass
79
-
81
+
80
82
  @abstractmethod
81
83
  def create_gauge(self, name: str, description: str = "", unit: str = "") -> Gauge:
82
84
  """Create a gauge instrument."""
83
85
  pass
84
-
86
+
85
87
  @abstractmethod
86
88
  def create_histogram(self, name: str, description: str = "", unit: str = "") -> Histogram:
87
89
  """Create a histogram instrument."""
88
90
  pass
89
-
91
+
90
92
  # Convenience methods (simple, pass name each time)
91
- def increment_counter(self, name: str, value: float = 1, attributes: Optional[dict[str, Any]] = None) -> None:
93
+ def increment_counter(
94
+ self, name: str, value: float = 1, attributes: Optional[dict[str, Any]] = None
95
+ ) -> None:
92
96
  """Convenience method to increment a counter."""
93
97
  counter = self.create_counter(name)
94
98
  counter.add(value, attributes)
95
-
96
- def update_updown_counter(self, name: str, value: float, attributes: Optional[dict[str, Any]] = None) -> None:
99
+
100
+ def update_updown_counter(
101
+ self, name: str, value: float, attributes: Optional[dict[str, Any]] = None
102
+ ) -> None:
97
103
  """Convenience method to update an up/down counter."""
98
104
  counter = self.create_updown_counter(name)
99
105
  counter.add(value, attributes)
100
-
101
- def record_gauge(self, name: str, value: float, attributes: Optional[dict[str, Any]] = None) -> None:
106
+
107
+ def record_gauge(
108
+ self, name: str, value: float, attributes: Optional[dict[str, Any]] = None
109
+ ) -> None:
102
110
  """Convenience method to record a gauge value."""
103
111
  gauge = self.create_gauge(name)
104
112
  gauge.set(value, attributes)
105
-
106
- def record_histogram(self, name: str, value: float, attributes: Optional[dict[str, Any]] = None) -> None:
113
+
114
+ def record_histogram(
115
+ self, name: str, value: float, attributes: Optional[dict[str, Any]] = None
116
+ ) -> None:
107
117
  """Convenience method to record a histogram value."""
108
118
  histogram = self.create_histogram(name)
109
119
  histogram.record(value, attributes)
110
-
120
+
111
121
  # Span methods
112
122
  @abstractmethod
113
123
  def start_span(self, name: str, attributes: Optional[dict[str, Any]] = None) -> Span:
114
124
  """Start a new span."""
115
125
  pass
116
-
126
+
117
127
  # Context manager for spans
118
128
  @contextmanager
119
129
  def span(self, name: str, attributes: Optional[dict[str, Any]] = None):
@@ -129,27 +139,33 @@ class InstrumentationProvider(ABC):
129
139
  raise
130
140
  else:
131
141
  span.end("ok")
132
-
142
+
133
143
  # Decorator for tracing functions
134
144
  def trace(self, name: Optional[str] = None, attributes: Optional[dict[str, Any]] = None):
135
145
  """Decorator to trace function execution."""
146
+
136
147
  def decorator(func: Callable):
137
148
  span_name = name or f"{func.__module__}.{func.__name__}"
138
-
149
+
139
150
  if asyncio.iscoroutinefunction(func):
151
+
140
152
  @functools.wraps(func)
141
153
  async def async_wrapper(*args, **kwargs):
142
154
  with self.span(span_name, attributes):
143
155
  return await func(*args, **kwargs)
156
+
144
157
  return async_wrapper
145
158
  else:
159
+
146
160
  @functools.wraps(func)
147
161
  def sync_wrapper(*args, **kwargs):
148
162
  with self.span(span_name, attributes):
149
163
  return func(*args, **kwargs)
164
+
150
165
  return sync_wrapper
166
+
151
167
  return decorator
152
-
168
+
153
169
  # Timing helpers
154
170
  @contextmanager
155
171
  def time_histogram(self, name: str, attributes: Optional[dict[str, Any]] = None):
@@ -161,22 +177,30 @@ class InstrumentationProvider(ABC):
161
177
  finally:
162
178
  duration = time.perf_counter() - start
163
179
  histogram.record(duration, attributes)
164
-
165
- def time_function(self, histogram_name: Optional[str] = None, attributes: Optional[dict[str, Any]] = None):
180
+
181
+ def time_function(
182
+ self, histogram_name: Optional[str] = None, attributes: Optional[dict[str, Any]] = None
183
+ ):
166
184
  """Decorator to time function execution."""
185
+
167
186
  def decorator(func: Callable):
168
187
  name = histogram_name or f"{func.__module__}.{func.__name__}.duration"
169
-
188
+
170
189
  if asyncio.iscoroutinefunction(func):
190
+
171
191
  @functools.wraps(func)
172
192
  async def async_wrapper(*args, **kwargs):
173
193
  with self.time_histogram(name, attributes):
174
194
  return await func(*args, **kwargs)
195
+
175
196
  return async_wrapper
176
197
  else:
198
+
177
199
  @functools.wraps(func)
178
200
  def sync_wrapper(*args, **kwargs):
179
201
  with self.time_histogram(name, attributes):
180
202
  return func(*args, **kwargs)
203
+
181
204
  return sync_wrapper
182
- return decorator
205
+
206
+ return decorator
@@ -1,19 +1,20 @@
1
1
  """No-operation implementation of instrumentation interfaces."""
2
2
 
3
3
  from typing import Any, Optional
4
+
4
5
  from .interfaces import (
5
- InstrumentationProvider,
6
6
  Counter,
7
- UpDownCounter,
8
7
  Gauge,
9
8
  Histogram,
9
+ InstrumentationProvider,
10
10
  Span,
11
+ UpDownCounter,
11
12
  )
12
13
 
13
14
 
14
15
  class NoOpCounter(Counter):
15
16
  """No-op counter implementation."""
16
-
17
+
17
18
  def add(self, value: float = 1, attributes: Optional[dict[str, Any]] = None) -> None:
18
19
  """No-op: does nothing."""
19
20
  pass
@@ -21,7 +22,7 @@ class NoOpCounter(Counter):
21
22
 
22
23
  class NoOpUpDownCounter(UpDownCounter):
23
24
  """No-op up/down counter implementation."""
24
-
25
+
25
26
  def add(self, value: float, attributes: Optional[dict[str, Any]] = None) -> None:
26
27
  """No-op: does nothing."""
27
28
  pass
@@ -29,7 +30,7 @@ class NoOpUpDownCounter(UpDownCounter):
29
30
 
30
31
  class NoOpGauge(Gauge):
31
32
  """No-op gauge implementation."""
32
-
33
+
33
34
  def set(self, value: float, attributes: Optional[dict[str, Any]] = None) -> None:
34
35
  """No-op: does nothing."""
35
36
  pass
@@ -37,7 +38,7 @@ class NoOpGauge(Gauge):
37
38
 
38
39
  class NoOpHistogram(Histogram):
39
40
  """No-op histogram implementation."""
40
-
41
+
41
42
  def record(self, value: float, attributes: Optional[dict[str, Any]] = None) -> None:
42
43
  """No-op: does nothing."""
43
44
  pass
@@ -45,15 +46,15 @@ class NoOpHistogram(Histogram):
45
46
 
46
47
  class NoOpSpan(Span):
47
48
  """No-op span implementation."""
48
-
49
+
49
50
  def set_attribute(self, key: str, value: Any) -> None:
50
51
  """No-op: does nothing."""
51
52
  pass
52
-
53
+
53
54
  def add_event(self, name: str, attributes: Optional[dict[str, Any]] = None) -> None:
54
55
  """No-op: does nothing."""
55
56
  pass
56
-
57
+
57
58
  def end(self, status: str = "ok") -> None:
58
59
  """No-op: does nothing."""
59
60
  pass
@@ -62,11 +63,11 @@ class NoOpSpan(Span):
62
63
  class NoOpInstrumentation(InstrumentationProvider):
63
64
  """
64
65
  Default no-operation instrumentation provider.
65
-
66
+
66
67
  This implementation does nothing and has zero performance overhead.
67
68
  It's used as the default when no instrumentation is configured.
68
69
  """
69
-
70
+
70
71
  def __init__(self):
71
72
  """Initialize the no-op provider."""
72
73
  # Cache single instances to avoid object creation overhead
@@ -75,23 +76,25 @@ class NoOpInstrumentation(InstrumentationProvider):
75
76
  self._gauge = NoOpGauge()
76
77
  self._histogram = NoOpHistogram()
77
78
  self._span = NoOpSpan()
78
-
79
+
79
80
  def create_counter(self, name: str, description: str = "", unit: str = "") -> Counter:
80
81
  """Return a no-op counter."""
81
82
  return self._counter
82
-
83
- def create_updown_counter(self, name: str, description: str = "", unit: str = "") -> UpDownCounter:
83
+
84
+ def create_updown_counter(
85
+ self, name: str, description: str = "", unit: str = ""
86
+ ) -> UpDownCounter:
84
87
  """Return a no-op up/down counter."""
85
88
  return self._updown_counter
86
-
89
+
87
90
  def create_gauge(self, name: str, description: str = "", unit: str = "") -> Gauge:
88
91
  """Return a no-op gauge."""
89
92
  return self._gauge
90
-
93
+
91
94
  def create_histogram(self, name: str, description: str = "", unit: str = "") -> Histogram:
92
95
  """Return a no-op histogram."""
93
96
  return self._histogram
94
-
97
+
95
98
  def start_span(self, name: str, attributes: Optional[dict[str, Any]] = None) -> Span:
96
99
  """Return a no-op span."""
97
- return self._span
100
+ return self._span