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.
- pyview/__init__.py +16 -6
- pyview/assets/js/app.js +1 -0
- pyview/assets/js/uploaders.js +221 -0
- pyview/assets/package-lock.json +16 -14
- pyview/assets/package.json +2 -2
- pyview/async_stream_runner.py +2 -1
- pyview/auth/__init__.py +3 -1
- pyview/auth/provider.py +6 -6
- pyview/auth/required.py +7 -10
- pyview/binding/__init__.py +47 -0
- pyview/binding/binder.py +134 -0
- pyview/binding/context.py +33 -0
- pyview/binding/converters.py +191 -0
- pyview/binding/helpers.py +78 -0
- pyview/binding/injectables.py +119 -0
- pyview/binding/params.py +105 -0
- pyview/binding/result.py +32 -0
- pyview/changesets/__init__.py +2 -0
- pyview/changesets/changesets.py +8 -3
- pyview/cli/commands/create_view.py +4 -3
- pyview/cli/main.py +1 -1
- pyview/components/__init__.py +72 -0
- pyview/components/base.py +212 -0
- pyview/components/lifecycle.py +85 -0
- pyview/components/manager.py +366 -0
- pyview/components/renderer.py +14 -0
- pyview/components/slots.py +73 -0
- pyview/csrf.py +4 -2
- pyview/events/AutoEventDispatch.py +98 -0
- pyview/events/BaseEventHandler.py +51 -8
- pyview/events/__init__.py +2 -1
- pyview/instrumentation/__init__.py +3 -3
- pyview/instrumentation/interfaces.py +57 -33
- pyview/instrumentation/noop.py +21 -18
- pyview/js.py +20 -23
- pyview/live_routes.py +5 -3
- pyview/live_socket.py +167 -44
- pyview/live_view.py +24 -12
- pyview/meta.py +14 -2
- pyview/phx_message.py +7 -8
- pyview/playground/__init__.py +10 -0
- pyview/playground/builder.py +118 -0
- pyview/playground/favicon.py +39 -0
- pyview/pyview.py +54 -20
- pyview/session.py +2 -0
- pyview/static/assets/app.js +2088 -806
- pyview/static/assets/uploaders.js +221 -0
- pyview/stream.py +308 -0
- pyview/template/__init__.py +11 -1
- pyview/template/live_template.py +12 -8
- pyview/template/live_view_template.py +338 -0
- pyview/template/render_diff.py +33 -7
- pyview/template/root_template.py +21 -9
- pyview/template/serializer.py +2 -5
- pyview/template/template_view.py +170 -0
- pyview/template/utils.py +3 -2
- pyview/uploads.py +344 -55
- pyview/vendor/flet/pubsub/__init__.py +3 -1
- pyview/vendor/flet/pubsub/pub_sub.py +10 -18
- pyview/vendor/ibis/__init__.py +3 -7
- pyview/vendor/ibis/compiler.py +25 -32
- pyview/vendor/ibis/context.py +13 -15
- pyview/vendor/ibis/errors.py +0 -6
- pyview/vendor/ibis/filters.py +70 -76
- pyview/vendor/ibis/loaders.py +6 -7
- pyview/vendor/ibis/nodes.py +40 -42
- pyview/vendor/ibis/template.py +4 -5
- pyview/vendor/ibis/tree.py +62 -3
- pyview/vendor/ibis/utils.py +14 -15
- pyview/ws_handler.py +116 -86
- {pyview_web-0.3.0.dist-info → pyview_web-0.8.0a2.dist-info}/METADATA +39 -33
- pyview_web-0.8.0a2.dist-info/RECORD +80 -0
- pyview_web-0.8.0a2.dist-info/WHEEL +4 -0
- pyview_web-0.8.0a2.dist-info/entry_points.txt +3 -0
- pyview_web-0.3.0.dist-info/LICENSE +0 -21
- pyview_web-0.3.0.dist-info/RECORD +0 -58
- pyview_web-0.3.0.dist-info/WHEEL +0 -4
- 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
|
-
"""
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
205
|
+
|
|
206
|
+
return decorator
|
pyview/instrumentation/noop.py
CHANGED
|
@@ -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(
|
|
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
|