hmdl 0.0.1__py3-none-any.whl → 0.0.2__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.
hmdl/__init__.py CHANGED
@@ -6,7 +6,7 @@ OpenTelemetry-based observability tracking.
6
6
  """
7
7
 
8
8
  from hmdl.client import HeimdallClient
9
- from hmdl.decorators import trace_mcp_tool
9
+ from hmdl.decorators import trace_mcp_tool, UserExtractor, SessionExtractor
10
10
  from hmdl.config import HeimdallConfig
11
11
  from hmdl.types import SpanKind, SpanStatus
12
12
 
@@ -17,6 +17,8 @@ __all__ = [
17
17
  "HeimdallClient",
18
18
  # Decorators
19
19
  "trace_mcp_tool",
20
+ "UserExtractor",
21
+ "SessionExtractor",
20
22
  # Configuration
21
23
  "HeimdallConfig",
22
24
  # Types
hmdl/client.py CHANGED
@@ -20,27 +20,31 @@ logger = logging.getLogger(__name__)
20
20
 
21
21
  class HeimdallClient:
22
22
  """Client for sending observability data to Heimdall platform.
23
-
23
+
24
24
  This client sets up OpenTelemetry tracing and provides methods for
25
25
  creating spans and recording MCP operations.
26
-
26
+
27
27
  Example:
28
28
  >>> from hmdl import HeimdallClient
29
29
  >>> client = HeimdallClient(api_key="your-api-key")
30
30
  >>> with client.start_span("my-operation") as span:
31
31
  ... # Your code here
32
32
  ... span.set_attribute("custom.attribute", "value")
33
+
34
+ # Track user sessions
35
+ >>> client.set_session_id("session-123")
36
+ >>> client.set_user_id("user-456")
33
37
  """
34
-
38
+
35
39
  _instance: Optional["HeimdallClient"] = None
36
40
  _initialized: bool = False
37
-
41
+
38
42
  def __new__(cls, *args: Any, **kwargs: Any) -> "HeimdallClient":
39
43
  """Singleton pattern to ensure only one client instance."""
40
44
  if cls._instance is None:
41
45
  cls._instance = super().__new__(cls)
42
46
  return cls._instance
43
-
47
+
44
48
  def __init__(
45
49
  self,
46
50
  config: Optional[HeimdallConfig] = None,
@@ -50,6 +54,8 @@ class HeimdallClient:
50
54
  environment: Optional[str] = None,
51
55
  org_id: Optional[str] = None,
52
56
  project_id: Optional[str] = None,
57
+ session_id: Optional[str] = None,
58
+ user_id: Optional[str] = None,
53
59
  ) -> None:
54
60
  """Initialize the Heimdall client.
55
61
 
@@ -61,6 +67,8 @@ class HeimdallClient:
61
67
  environment: Deployment environment.
62
68
  org_id: Organization ID from Heimdall dashboard.
63
69
  project_id: Project ID from Heimdall dashboard.
70
+ session_id: Session ID for tracking MCP client sessions.
71
+ user_id: User ID for tracking users.
64
72
  """
65
73
  if self._initialized:
66
74
  return
@@ -76,16 +84,22 @@ class HeimdallClient:
76
84
  environment=environment or HeimdallConfig().environment,
77
85
  org_id=org_id or HeimdallConfig().org_id,
78
86
  project_id=project_id or HeimdallConfig().project_id,
87
+ session_id=session_id or HeimdallConfig().session_id,
88
+ user_id=user_id or HeimdallConfig().user_id,
79
89
  )
80
-
90
+
81
91
  self._tracer: Optional[trace.Tracer] = None
82
92
  self._provider: Optional[TracerProvider] = None
83
-
93
+
94
+ # Runtime session and user tracking (can be updated dynamically)
95
+ self._session_id: Optional[str] = self.config.session_id
96
+ self._user_id: Optional[str] = self.config.user_id
97
+
84
98
  if self.config.enabled:
85
99
  self._setup_tracing()
86
-
100
+
87
101
  self._initialized = True
88
-
102
+
89
103
  # Register cleanup on exit
90
104
  atexit.register(self.shutdown)
91
105
 
@@ -181,6 +195,45 @@ class HeimdallClient:
181
195
  self._provider.shutdown()
182
196
  logger.debug("Heimdall client shutdown complete")
183
197
 
198
+ def get_session_id(self) -> Optional[str]:
199
+ """Get the current session ID."""
200
+ return self._session_id
201
+
202
+ def set_session_id(self, session_id: Optional[str]) -> None:
203
+ """Set the session ID for all subsequent spans.
204
+
205
+ Call this when an MCP client connects to associate all operations
206
+ with that session.
207
+
208
+ Args:
209
+ session_id: The session identifier (e.g., from MCP client connection)
210
+
211
+ Example:
212
+ >>> # When MCP client connects
213
+ >>> client.set_session_id(ctx.session_id or ctx.client_info.name)
214
+ """
215
+ self._session_id = session_id
216
+ logger.debug(f"Session ID set to: {session_id}")
217
+
218
+ def get_user_id(self) -> Optional[str]:
219
+ """Get the current user ID."""
220
+ return self._user_id
221
+
222
+ def set_user_id(self, user_id: Optional[str]) -> None:
223
+ """Set the user ID for all subsequent spans.
224
+
225
+ Can be overridden per-span using user_extractor option in decorators.
226
+
227
+ Args:
228
+ user_id: The user identifier
229
+
230
+ Example:
231
+ >>> # When user is identified
232
+ >>> client.set_user_id("user-123")
233
+ """
234
+ self._user_id = user_id
235
+ logger.debug(f"User ID set to: {user_id}")
236
+
184
237
  @classmethod
185
238
  def get_instance(cls) -> Optional["HeimdallClient"]:
186
239
  """Get the singleton client instance."""
hmdl/config.py CHANGED
@@ -18,6 +18,10 @@ class HeimdallConfig:
18
18
  environment: Deployment environment (e.g., 'production', 'staging').
19
19
  org_id: Organization ID from Heimdall dashboard.
20
20
  project_id: Project ID to associate traces with in Heimdall.
21
+ session_id: Session ID to associate with all spans. Useful for tracking
22
+ requests from the same MCP client session.
23
+ user_id: User ID to associate with all spans. Can be overridden per-span
24
+ using user_extractor option in decorators.
21
25
  enabled: Whether tracing is enabled.
22
26
  debug: Enable debug logging.
23
27
  batch_size: Number of spans to batch before sending.
@@ -31,7 +35,7 @@ class HeimdallConfig:
31
35
  )
32
36
  endpoint: str = field(
33
37
  default_factory=lambda: os.environ.get(
34
- "HEIMDALL_ENDPOINT", "https://api.heimdall.dev"
38
+ "HEIMDALL_ENDPOINT", "http://localhost:4318"
35
39
  )
36
40
  )
37
41
  service_name: str = field(
@@ -46,6 +50,12 @@ class HeimdallConfig:
46
50
  project_id: str = field(
47
51
  default_factory=lambda: os.environ.get("HEIMDALL_PROJECT_ID", "default")
48
52
  )
53
+ session_id: Optional[str] = field(
54
+ default_factory=lambda: os.environ.get("HEIMDALL_SESSION_ID")
55
+ )
56
+ user_id: Optional[str] = field(
57
+ default_factory=lambda: os.environ.get("HEIMDALL_USER_ID")
58
+ )
49
59
  enabled: bool = field(
50
60
  default_factory=lambda: os.environ.get("HEIMDALL_ENABLED", "true").lower() == "true"
51
61
  )
hmdl/decorators.py CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import base64
5
6
  import functools
6
7
  import inspect
7
8
  import json
8
9
  import time
9
- from typing import Any, Callable, Optional, TypeVar, Union, overload
10
+ from typing import Any, Callable, Dict, Optional, TypeVar, Union, overload
10
11
 
11
12
  from opentelemetry import trace
12
13
  from opentelemetry.trace import Status, StatusCode
@@ -15,6 +16,64 @@ from hmdl.types import HeimdallAttributes, SpanKind, SpanStatus
15
16
 
16
17
  F = TypeVar("F", bound=Callable[..., Any])
17
18
 
19
+ # Type alias for user extractor function
20
+ # Takes (args, kwargs) and returns user ID string or None
21
+ UserExtractor = Callable[[tuple, dict], Optional[str]]
22
+
23
+ # Type alias for session extractor function
24
+ # Takes (args, kwargs) and returns session ID string or None
25
+ SessionExtractor = Callable[[tuple, dict], Optional[str]]
26
+
27
+ # MCP header names
28
+ MCP_SESSION_ID_HEADER = "Mcp-Session-Id"
29
+ AUTHORIZATION_HEADER = "Authorization"
30
+
31
+
32
+ def _parse_jwt_claims(token: str) -> Dict[str, Any]:
33
+ """Parse JWT claims from a token string (without verification)."""
34
+ try:
35
+ token_str = token
36
+ if token_str.lower().startswith("bearer "):
37
+ token_str = token_str[7:]
38
+ parts = token_str.split(".")
39
+ if len(parts) != 3:
40
+ return {}
41
+ payload = parts[1]
42
+ # Add padding if needed
43
+ padding = 4 - len(payload) % 4
44
+ if padding != 4:
45
+ payload += "=" * padding
46
+ decoded = base64.urlsafe_b64decode(payload).decode("utf-8")
47
+ return json.loads(decoded)
48
+ except Exception:
49
+ return {}
50
+
51
+
52
+ def _extract_user_id_from_token(token: str) -> Optional[str]:
53
+ """Extract user ID from a JWT token."""
54
+ claims = _parse_jwt_claims(token)
55
+ # Check common user ID claims in order of preference
56
+ for claim in ["sub", "user_id", "userId", "uid"]:
57
+ if claim in claims and isinstance(claims[claim], str):
58
+ return claims[claim]
59
+ return None
60
+
61
+
62
+ def _extract_from_headers(headers: Dict[str, str]) -> tuple[Optional[str], Optional[str]]:
63
+ """Extract session ID and user ID from HTTP headers.
64
+
65
+ Returns:
66
+ Tuple of (session_id, user_id)
67
+ """
68
+ # Normalize headers to lowercase for case-insensitive lookup
69
+ normalized = {k.lower(): v for k, v in headers.items()}
70
+
71
+ session_id = normalized.get(MCP_SESSION_ID_HEADER.lower())
72
+ auth_header = normalized.get(AUTHORIZATION_HEADER.lower())
73
+ user_id = _extract_user_id_from_token(auth_header) if auth_header else None
74
+
75
+ return session_id, user_id
76
+
18
77
 
19
78
  def _serialize_value(value: Any) -> str:
20
79
  """Safely serialize a value to string for span attributes."""
@@ -30,52 +89,128 @@ def _get_client() -> Any:
30
89
  return HeimdallClient.get_instance()
31
90
 
32
91
 
92
+ def _extract_session_id(
93
+ args: tuple,
94
+ kwargs: dict,
95
+ session_extractor: Optional[SessionExtractor],
96
+ header_session_id: Optional[str],
97
+ ) -> Optional[str]:
98
+ """Extract session ID using the extractor callback or headers.
99
+
100
+ Priority: session_extractor callback > headers > None
101
+ """
102
+ # Try extractor callback first
103
+ if session_extractor:
104
+ try:
105
+ result = session_extractor(args, kwargs)
106
+ if result:
107
+ return result
108
+ except Exception:
109
+ # Ignore extraction errors
110
+ pass
111
+
112
+ # Fall back to headers
113
+ if header_session_id:
114
+ return header_session_id
115
+
116
+ return None
117
+
118
+
119
+ def _extract_user_id(
120
+ args: tuple,
121
+ kwargs: dict,
122
+ user_extractor: Optional[UserExtractor],
123
+ header_user_id: Optional[str],
124
+ ) -> Optional[str]:
125
+ """Extract user ID using the extractor callback or headers.
126
+
127
+ Priority: user_extractor callback > headers > None
128
+ """
129
+ # Try extractor callback first
130
+ if user_extractor:
131
+ try:
132
+ result = user_extractor(args, kwargs)
133
+ if result:
134
+ return result
135
+ except Exception:
136
+ # Ignore extraction errors
137
+ pass
138
+
139
+ # Fall back to headers
140
+ if header_user_id:
141
+ return header_user_id
142
+
143
+ return None
144
+
145
+
33
146
  def _create_span_decorator(
34
147
  span_kind: SpanKind,
35
148
  name_attr: str,
36
149
  args_attr: str,
37
150
  result_attr: str,
38
- ) -> Callable[[Optional[str]], Callable[[F], F]]:
151
+ ) -> Callable[..., Callable[[F], F]]:
39
152
  """Factory for creating MCP-specific decorators."""
40
-
41
- def decorator(name: Optional[str] = None) -> Callable[[F], F]:
153
+
154
+ def decorator(
155
+ name: Optional[str] = None,
156
+ *,
157
+ headers: Optional[Dict[str, str]] = None,
158
+ user_extractor: Optional[UserExtractor] = None,
159
+ session_extractor: Optional[SessionExtractor] = None,
160
+ ) -> Callable[[F], F]:
161
+ # Pre-extract from headers if provided
162
+ header_session_id, header_user_id = _extract_from_headers(headers) if headers else (None, None)
163
+
42
164
  def wrapper(func: F) -> F:
43
165
  span_name = name or func.__name__
44
166
  is_async = inspect.iscoroutinefunction(func)
45
-
167
+
46
168
  if is_async:
47
169
  @functools.wraps(func)
48
170
  async def async_wrapped(*args: Any, **kwargs: Any) -> Any:
49
171
  client = _get_client()
50
172
  if client is None:
51
173
  return await func(*args, **kwargs)
52
-
174
+
53
175
  tracer = client.tracer
54
176
  with tracer.start_as_current_span(
55
177
  name=span_name,
56
178
  kind=trace.SpanKind.SERVER,
57
179
  ) as span:
58
180
  start_time = time.perf_counter()
59
-
181
+
60
182
  # Set input attributes
61
183
  span.set_attribute(name_attr, span_name)
62
184
  span.set_attribute("heimdall.span_kind", span_kind.value)
63
-
185
+
186
+ # Extract session ID - priority: extractor > headers > client
187
+ session_id = _extract_session_id(args, kwargs, session_extractor, header_session_id)
188
+ if not session_id:
189
+ session_id = client.get_session_id()
190
+ if session_id:
191
+ span.set_attribute(HeimdallAttributes.HEIMDALL_SESSION_ID, session_id)
192
+
193
+ # Extract user ID - priority: extractor > headers > client > "anonymous"
194
+ user_id = _extract_user_id(args, kwargs, user_extractor, header_user_id)
195
+ if not user_id:
196
+ user_id = client.get_user_id()
197
+ span.set_attribute(HeimdallAttributes.HEIMDALL_USER_ID, user_id or "anonymous")
198
+
64
199
  # Capture arguments
65
200
  try:
66
201
  all_args = _capture_arguments(func, args, kwargs)
67
202
  span.set_attribute(args_attr, _serialize_value(all_args))
68
203
  except Exception:
69
204
  pass
70
-
205
+
71
206
  try:
72
207
  result = await func(*args, **kwargs)
73
-
208
+
74
209
  # Set output attributes
75
210
  span.set_attribute(result_attr, _serialize_value(result))
76
211
  span.set_attribute(HeimdallAttributes.STATUS, SpanStatus.OK.value)
77
212
  span.set_status(Status(StatusCode.OK))
78
-
213
+
79
214
  return result
80
215
  except Exception as e:
81
216
  _record_error(span, e)
@@ -83,7 +218,7 @@ def _create_span_decorator(
83
218
  finally:
84
219
  duration_ms = (time.perf_counter() - start_time) * 1000
85
220
  span.set_attribute(HeimdallAttributes.DURATION_MS, duration_ms)
86
-
221
+
87
222
  return async_wrapped # type: ignore
88
223
  else:
89
224
  @functools.wraps(func)
@@ -91,33 +226,46 @@ def _create_span_decorator(
91
226
  client = _get_client()
92
227
  if client is None:
93
228
  return func(*args, **kwargs)
94
-
229
+
95
230
  tracer = client.tracer
96
231
  with tracer.start_as_current_span(
97
232
  name=span_name,
98
233
  kind=trace.SpanKind.SERVER,
99
234
  ) as span:
100
235
  start_time = time.perf_counter()
101
-
236
+
102
237
  # Set input attributes
103
238
  span.set_attribute(name_attr, span_name)
104
239
  span.set_attribute("heimdall.span_kind", span_kind.value)
105
-
240
+
241
+ # Extract session ID - priority: extractor > headers > client
242
+ session_id = _extract_session_id(args, kwargs, session_extractor, header_session_id)
243
+ if not session_id:
244
+ session_id = client.get_session_id()
245
+ if session_id:
246
+ span.set_attribute(HeimdallAttributes.HEIMDALL_SESSION_ID, session_id)
247
+
248
+ # Extract user ID - priority: extractor > headers > client > "anonymous"
249
+ user_id = _extract_user_id(args, kwargs, user_extractor, header_user_id)
250
+ if not user_id:
251
+ user_id = client.get_user_id()
252
+ span.set_attribute(HeimdallAttributes.HEIMDALL_USER_ID, user_id or "anonymous")
253
+
106
254
  # Capture arguments
107
255
  try:
108
256
  all_args = _capture_arguments(func, args, kwargs)
109
257
  span.set_attribute(args_attr, _serialize_value(all_args))
110
258
  except Exception:
111
259
  pass
112
-
260
+
113
261
  try:
114
262
  result = func(*args, **kwargs)
115
-
263
+
116
264
  # Set output attributes
117
265
  span.set_attribute(result_attr, _serialize_value(result))
118
266
  span.set_attribute(HeimdallAttributes.STATUS, SpanStatus.OK.value)
119
267
  span.set_status(Status(StatusCode.OK))
120
-
268
+
121
269
  return result
122
270
  except Exception as e:
123
271
  _record_error(span, e)
@@ -125,11 +273,11 @@ def _create_span_decorator(
125
273
  finally:
126
274
  duration_ms = (time.perf_counter() - start_time) * 1000
127
275
  span.set_attribute(HeimdallAttributes.DURATION_MS, duration_ms)
128
-
276
+
129
277
  return sync_wrapped # type: ignore
130
-
278
+
131
279
  return wrapper
132
-
280
+
133
281
  return decorator
134
282
 
135
283
 
@@ -160,6 +308,15 @@ trace_mcp_tool = _create_span_decorator(
160
308
  trace_mcp_tool.__doc__ = """
161
309
  Decorator to trace MCP tool calls.
162
310
 
311
+ Args:
312
+ name: Custom name for the span (defaults to function name)
313
+ user_extractor: Function to extract user ID from (args, kwargs).
314
+ Useful for extracting user info from MCP Context.
315
+ Returns user ID string or None to use default from client.
316
+ session_extractor: Function to extract session ID from (args, kwargs).
317
+ Useful for extracting session info from MCP Context.
318
+ Returns session ID string or None to use default from client.
319
+
163
320
  Example:
164
321
  >>> @trace_mcp_tool()
165
322
  ... def my_tool(arg1: str, arg2: int) -> str:
@@ -168,6 +325,14 @@ Example:
168
325
  >>> @trace_mcp_tool("custom-tool-name")
169
326
  ... async def async_tool(data: dict) -> dict:
170
327
  ... return {"processed": data}
328
+
329
+ # Extract user and session from MCP Context (first argument)
330
+ >>> @trace_mcp_tool(
331
+ ... user_extractor=lambda args, kwargs: getattr(args[0], 'user_id', None) if args else None,
332
+ ... session_extractor=lambda args, kwargs: getattr(args[0], 'session_id', None) if args else None,
333
+ ... )
334
+ ... def my_tool_with_ctx(ctx, query: str) -> str:
335
+ ... return f"Query: {query}"
171
336
  """
172
337
 
173
338
  trace_mcp_resource = _create_span_decorator(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hmdl
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: Observability SDK for MCP (Model Context Protocol) servers - Heimdall Platform
5
5
  Project-URL: Homepage, https://tryheimdall.com
6
6
  Project-URL: Documentation, https://docs.tryheimdall.com
@@ -141,6 +141,8 @@ async def async_search(query: str) -> list:
141
141
  | `HEIMDALL_DEBUG` | Enable debug logging | `false` |
142
142
  | `HEIMDALL_BATCH_SIZE` | Spans per batch | `100` |
143
143
  | `HEIMDALL_FLUSH_INTERVAL_MS` | Flush interval (ms) | `5000` |
144
+ | `HEIMDALL_SESSION_ID` | Default session ID | - |
145
+ | `HEIMDALL_USER_ID` | Default user ID | - |
144
146
 
145
147
  ### Local Development
146
148
 
@@ -155,6 +157,45 @@ export HEIMDALL_ENABLED="true"
155
157
 
156
158
  ## Advanced Usage
157
159
 
160
+ ### Session and User Tracking
161
+
162
+ `trace_mcp_tool` automatically includes session and user IDs in spans. You just need to provide them via one of these methods:
163
+
164
+ #### Option 1: HTTP Headers (Recommended for MCP servers)
165
+
166
+ Pass HTTP headers directly to `trace_mcp_tool`. Session ID is extracted from the `Mcp-Session-Id` header, and user ID from the JWT token in the `Authorization` header:
167
+
168
+ ```python
169
+ from hmdl import trace_mcp_tool
170
+
171
+ @app.post("/mcp")
172
+ def handle_request():
173
+ @trace_mcp_tool(headers=dict(request.headers))
174
+ def search_tool(query: str):
175
+ return results
176
+
177
+ return search_tool("test") # Session/user included in span
178
+ ```
179
+
180
+ #### Option 2: Extractors (Per-tool extraction)
181
+
182
+ ```python
183
+ from typing import Optional
184
+
185
+ @trace_mcp_tool(
186
+ session_extractor=lambda args, kwargs: kwargs.get('session_id'),
187
+ user_extractor=lambda args, kwargs: kwargs.get('user_id'),
188
+ )
189
+ def my_tool(query: str, session_id: Optional[str] = None, user_id: Optional[str] = None):
190
+ return f"Query: {query}"
191
+ ```
192
+
193
+ #### Resolution Priority
194
+
195
+ 1. Extractor callback → 2. HTTP headers → 3. Client value (initialized from environment variables)
196
+
197
+ > **Note**: If no user ID is found through any of these methods, `"anonymous"` is used as the default.
198
+
158
199
  ### Custom span names
159
200
 
160
201
  ```python
@@ -0,0 +1,10 @@
1
+ hmdl/__init__.py,sha256=2OCWYFkHkNNRhYawHGVd9tUVfZ8WMMUuxhvJvgHsvFQ,648
2
+ hmdl/client.py,sha256=XItMSgojsDwz-NAjFzWF2dXHDo7o6rXKBHppbCgsSz0,8633
3
+ hmdl/config.py,sha256=crHQu_b2KO6WJ6vJ8GTtdCbuizAQopI5-WQtgaCHfj0,3520
4
+ hmdl/decorators.py,sha256=HupF7DUZRIqPPR8mvw2jCTv4US7fAQgAOuqPMlCGBNY,17561
5
+ hmdl/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
6
+ hmdl/types.py,sha256=C_QDl4YTJjnrhvMUdnDnTG35jKmovLFqMrHXL4LHOG0,3130
7
+ hmdl-0.0.2.dist-info/METADATA,sha256=pxcj7lxXZd1Bnu7aLnvOo6zhgWDhjM9Ldx9sQwIzTR4,8083
8
+ hmdl-0.0.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
9
+ hmdl-0.0.2.dist-info/licenses/LICENSE,sha256=b8jAb5oXJiKCT9GmhRp2uDLqZXIA63QnLT4_3JvzxhE,1064
10
+ hmdl-0.0.2.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- hmdl/__init__.py,sha256=EA49ssIg0WJTzi1-Oi0J8GWV6k55R-oav45xbufd4WU,570
2
- hmdl/client.py,sha256=_VHVIlfq8JV_FucLN9rjgj_0fE_C0h5DZWQCUPXSSCw,6782
3
- hmdl/config.py,sha256=jXo0XC962JM3-N2uUl6o8Dt0hUZjIWvDJcthm04Ux0U,3028
4
- hmdl/decorators.py,sha256=yUywG_AMCA-tqT-w4fO1bSpOrVn3TDk7Y981R2UmJjQ,11697
5
- hmdl/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
6
- hmdl/types.py,sha256=C_QDl4YTJjnrhvMUdnDnTG35jKmovLFqMrHXL4LHOG0,3130
7
- hmdl-0.0.1.dist-info/METADATA,sha256=gbak-CcmYZkcegGv0GqSvMaC41EtCo99IgWC4or3UE8,6743
8
- hmdl-0.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
9
- hmdl-0.0.1.dist-info/licenses/LICENSE,sha256=b8jAb5oXJiKCT9GmhRp2uDLqZXIA63QnLT4_3JvzxhE,1064
10
- hmdl-0.0.1.dist-info/RECORD,,
File without changes