azpaddypy 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,534 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
import json
|
4
|
+
import functools
|
5
|
+
import time
|
6
|
+
from typing import Optional, Dict, Any, Union, List
|
7
|
+
from datetime import datetime
|
8
|
+
from azure.monitor.opentelemetry import configure_azure_monitor
|
9
|
+
from opentelemetry import trace
|
10
|
+
from opentelemetry.trace import Status, StatusCode, Span
|
11
|
+
from opentelemetry.sdk.trace import TracerProvider
|
12
|
+
from opentelemetry.sdk.resources import Resource
|
13
|
+
|
14
|
+
|
15
|
+
class AzureLogger:
|
16
|
+
"""
|
17
|
+
Comprehensive logging class for Azure Functions and Azure Web Apps
|
18
|
+
using Azure Monitor OpenTelemetry integration with advanced tracing capabilities.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
service_name: str,
|
24
|
+
service_version: str = "1.0.0",
|
25
|
+
connection_string: Optional[str] = None,
|
26
|
+
log_level: int = logging.INFO,
|
27
|
+
enable_console_logging: bool = True,
|
28
|
+
custom_resource_attributes: Optional[Dict[str, str]] = None,
|
29
|
+
):
|
30
|
+
"""
|
31
|
+
Initialize the Azure Logger with OpenTelemetry tracing
|
32
|
+
|
33
|
+
Args:
|
34
|
+
service_name: Name of your service/application
|
35
|
+
service_version: Version of your service
|
36
|
+
connection_string: Application Insights connection string
|
37
|
+
log_level: Logging level (default: INFO)
|
38
|
+
enable_console_logging: Enable console output for local development
|
39
|
+
custom_resource_attributes: Additional resource attributes
|
40
|
+
"""
|
41
|
+
self.service_name = service_name
|
42
|
+
self.service_version = service_version
|
43
|
+
self.connection_string = connection_string or os.getenv(
|
44
|
+
"APPLICATIONINSIGHTS_CONNECTION_STRING"
|
45
|
+
)
|
46
|
+
|
47
|
+
# Prepare resource attributes
|
48
|
+
resource_attributes = {
|
49
|
+
"service.name": service_name,
|
50
|
+
"service.version": service_version,
|
51
|
+
"service.instance.id": os.getenv("WEBSITE_INSTANCE_ID", "local"),
|
52
|
+
}
|
53
|
+
|
54
|
+
if custom_resource_attributes:
|
55
|
+
resource_attributes.update(custom_resource_attributes)
|
56
|
+
|
57
|
+
# Configure Azure Monitor if connection string is available
|
58
|
+
if self.connection_string:
|
59
|
+
try:
|
60
|
+
configure_azure_monitor(
|
61
|
+
connection_string=self.connection_string,
|
62
|
+
resource_attributes=resource_attributes,
|
63
|
+
enable_live_metrics=True,
|
64
|
+
)
|
65
|
+
self._telemetry_enabled = True
|
66
|
+
except Exception as e:
|
67
|
+
print(f"Warning: Failed to configure Azure Monitor: {e}")
|
68
|
+
self._telemetry_enabled = False
|
69
|
+
else:
|
70
|
+
self._telemetry_enabled = False
|
71
|
+
print(
|
72
|
+
"Warning: No Application Insights connection string found. Telemetry disabled."
|
73
|
+
)
|
74
|
+
|
75
|
+
# Set up logger without instrumentor
|
76
|
+
self.logger = logging.getLogger(service_name)
|
77
|
+
self.logger.setLevel(log_level)
|
78
|
+
|
79
|
+
# Clear existing handlers to avoid duplicates
|
80
|
+
self.logger.handlers.clear()
|
81
|
+
|
82
|
+
# Add console handler for local development
|
83
|
+
if enable_console_logging:
|
84
|
+
self._setup_console_handler()
|
85
|
+
|
86
|
+
# Get tracer for manual span creation
|
87
|
+
self.tracer = trace.get_tracer(__name__)
|
88
|
+
|
89
|
+
# Initialize correlation context
|
90
|
+
self._correlation_id = None
|
91
|
+
|
92
|
+
self.info(
|
93
|
+
f"Azure Logger initialized for service '{service_name}' v{service_version}"
|
94
|
+
)
|
95
|
+
|
96
|
+
def _setup_console_handler(self):
|
97
|
+
"""Setup console logging handler with structured format"""
|
98
|
+
console_handler = logging.StreamHandler()
|
99
|
+
formatter = logging.Formatter(
|
100
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(pathname)s:%(lineno)d"
|
101
|
+
)
|
102
|
+
console_handler.setFormatter(formatter)
|
103
|
+
self.logger.addHandler(console_handler)
|
104
|
+
|
105
|
+
def set_correlation_id(self, correlation_id: str):
|
106
|
+
"""Set correlation ID for request tracking"""
|
107
|
+
self._correlation_id = correlation_id
|
108
|
+
|
109
|
+
def get_correlation_id(self) -> Optional[str]:
|
110
|
+
"""Get current correlation ID"""
|
111
|
+
return self._correlation_id
|
112
|
+
|
113
|
+
def _enhance_extra(self, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
114
|
+
"""Enhance log extra data with correlation and service info"""
|
115
|
+
enhanced_extra = {
|
116
|
+
"service_name": self.service_name,
|
117
|
+
"service_version": self.service_version,
|
118
|
+
"timestamp": datetime.utcnow().isoformat(),
|
119
|
+
}
|
120
|
+
|
121
|
+
if self._correlation_id:
|
122
|
+
enhanced_extra["correlation_id"] = self._correlation_id
|
123
|
+
|
124
|
+
# Add span context if available
|
125
|
+
current_span = trace.get_current_span()
|
126
|
+
if current_span and current_span.is_recording():
|
127
|
+
span_context = current_span.get_span_context()
|
128
|
+
enhanced_extra["trace_id"] = format(span_context.trace_id, "032x")
|
129
|
+
enhanced_extra["span_id"] = format(span_context.span_id, "016x")
|
130
|
+
|
131
|
+
if extra:
|
132
|
+
enhanced_extra.update(extra)
|
133
|
+
|
134
|
+
return enhanced_extra
|
135
|
+
|
136
|
+
def debug(self, message: str, extra: Optional[Dict[str, Any]] = None):
|
137
|
+
"""Log debug message with enhanced context"""
|
138
|
+
self.logger.debug(message, extra=self._enhance_extra(extra))
|
139
|
+
|
140
|
+
def info(self, message: str, extra: Optional[Dict[str, Any]] = None):
|
141
|
+
"""Log info message with enhanced context"""
|
142
|
+
self.logger.info(message, extra=self._enhance_extra(extra))
|
143
|
+
|
144
|
+
def warning(self, message: str, extra: Optional[Dict[str, Any]] = None):
|
145
|
+
"""Log warning message with enhanced context"""
|
146
|
+
self.logger.warning(message, extra=self._enhance_extra(extra))
|
147
|
+
|
148
|
+
def error(
|
149
|
+
self,
|
150
|
+
message: str,
|
151
|
+
extra: Optional[Dict[str, Any]] = None,
|
152
|
+
exc_info: bool = True,
|
153
|
+
):
|
154
|
+
"""Log error message with enhanced context and exception info"""
|
155
|
+
self.logger.error(message, extra=self._enhance_extra(extra), exc_info=exc_info)
|
156
|
+
|
157
|
+
def critical(self, message: str, extra: Optional[Dict[str, Any]] = None):
|
158
|
+
"""Log critical message with enhanced context"""
|
159
|
+
self.logger.critical(message, extra=self._enhance_extra(extra))
|
160
|
+
|
161
|
+
def log_function_execution(
|
162
|
+
self,
|
163
|
+
function_name: str,
|
164
|
+
duration_ms: float,
|
165
|
+
success: bool = True,
|
166
|
+
extra: Optional[Dict[str, Any]] = None,
|
167
|
+
):
|
168
|
+
"""
|
169
|
+
Log function execution metrics with performance data
|
170
|
+
|
171
|
+
Args:
|
172
|
+
function_name: Name of the executed function
|
173
|
+
duration_ms: Execution duration in milliseconds
|
174
|
+
success: Whether the function executed successfully
|
175
|
+
extra: Additional custom properties
|
176
|
+
"""
|
177
|
+
log_data = {
|
178
|
+
"function_name": function_name,
|
179
|
+
"duration_ms": duration_ms,
|
180
|
+
"success": success,
|
181
|
+
"performance_category": "function_execution",
|
182
|
+
}
|
183
|
+
|
184
|
+
if extra:
|
185
|
+
log_data.update(extra)
|
186
|
+
|
187
|
+
log_level = logging.INFO if success else logging.ERROR
|
188
|
+
message = f"Function '{function_name}' executed in {duration_ms:.2f}ms - {'SUCCESS' if success else 'FAILED'}"
|
189
|
+
|
190
|
+
if success:
|
191
|
+
self.info(message, extra=log_data)
|
192
|
+
else:
|
193
|
+
self.error(message, extra=log_data, exc_info=False)
|
194
|
+
|
195
|
+
def log_request(
|
196
|
+
self,
|
197
|
+
method: str,
|
198
|
+
url: str,
|
199
|
+
status_code: int,
|
200
|
+
duration_ms: float,
|
201
|
+
extra: Optional[Dict[str, Any]] = None,
|
202
|
+
):
|
203
|
+
"""
|
204
|
+
Log HTTP request with comprehensive details
|
205
|
+
|
206
|
+
Args:
|
207
|
+
method: HTTP method
|
208
|
+
url: Request URL
|
209
|
+
status_code: HTTP status code
|
210
|
+
duration_ms: Request duration in milliseconds
|
211
|
+
extra: Additional custom properties
|
212
|
+
"""
|
213
|
+
log_data = {
|
214
|
+
"http_method": method,
|
215
|
+
"http_url": str(url),
|
216
|
+
"http_status_code": status_code,
|
217
|
+
"duration_ms": duration_ms,
|
218
|
+
"request_category": "http_request",
|
219
|
+
}
|
220
|
+
|
221
|
+
if extra:
|
222
|
+
log_data.update(extra)
|
223
|
+
|
224
|
+
# Determine log level based on status code
|
225
|
+
if status_code < 400:
|
226
|
+
log_level = logging.INFO
|
227
|
+
status_text = "SUCCESS"
|
228
|
+
elif status_code < 500:
|
229
|
+
log_level = logging.WARNING
|
230
|
+
status_text = "CLIENT_ERROR"
|
231
|
+
else:
|
232
|
+
log_level = logging.ERROR
|
233
|
+
status_text = "SERVER_ERROR"
|
234
|
+
|
235
|
+
message = (
|
236
|
+
f"{method} {url} - {status_code} - {duration_ms:.2f}ms - {status_text}"
|
237
|
+
)
|
238
|
+
self.logger.log(log_level, message, extra=self._enhance_extra(log_data))
|
239
|
+
|
240
|
+
def create_span(
|
241
|
+
self,
|
242
|
+
span_name: str,
|
243
|
+
attributes: Optional[Dict[str, Union[str, int, float, bool]]] = None,
|
244
|
+
) -> Span:
|
245
|
+
"""
|
246
|
+
Create a new span for distributed tracing
|
247
|
+
|
248
|
+
Args:
|
249
|
+
span_name: Name of the span
|
250
|
+
attributes: Initial attributes for the span
|
251
|
+
|
252
|
+
Returns:
|
253
|
+
OpenTelemetry span context manager
|
254
|
+
"""
|
255
|
+
span = self.tracer.start_span(span_name)
|
256
|
+
|
257
|
+
# Add default attributes
|
258
|
+
span.set_attribute("service.name", self.service_name)
|
259
|
+
span.set_attribute("service.version", self.service_version)
|
260
|
+
|
261
|
+
if self._correlation_id:
|
262
|
+
span.set_attribute("correlation.id", self._correlation_id)
|
263
|
+
|
264
|
+
# Add custom attributes
|
265
|
+
if attributes:
|
266
|
+
for key, value in attributes.items():
|
267
|
+
span.set_attribute(key, value)
|
268
|
+
|
269
|
+
return span
|
270
|
+
|
271
|
+
def trace_function(
|
272
|
+
self,
|
273
|
+
function_name: Optional[str] = None,
|
274
|
+
log_args: bool = False,
|
275
|
+
log_result: bool = False,
|
276
|
+
log_execution: bool = True,
|
277
|
+
):
|
278
|
+
"""
|
279
|
+
Decorator to automatically trace function execution with comprehensive logging
|
280
|
+
|
281
|
+
Args:
|
282
|
+
function_name: Custom name for the span (defaults to function name)
|
283
|
+
log_args: Whether to log function arguments
|
284
|
+
log_result: Whether to log function result
|
285
|
+
log_execution: Whether to log execution metrics
|
286
|
+
"""
|
287
|
+
|
288
|
+
def decorator(func):
|
289
|
+
@functools.wraps(func)
|
290
|
+
def wrapper(*args, **kwargs):
|
291
|
+
span_name = function_name or f"{func.__module__}.{func.__name__}"
|
292
|
+
|
293
|
+
with self.tracer.start_as_current_span(span_name) as span:
|
294
|
+
# Add function metadata
|
295
|
+
span.set_attribute("function.name", func.__name__)
|
296
|
+
span.set_attribute("function.module", func.__module__)
|
297
|
+
span.set_attribute("service.name", self.service_name)
|
298
|
+
|
299
|
+
if self._correlation_id:
|
300
|
+
span.set_attribute("correlation.id", self._correlation_id)
|
301
|
+
|
302
|
+
if log_args and args:
|
303
|
+
span.set_attribute("function.args_count", len(args))
|
304
|
+
if log_args and kwargs:
|
305
|
+
span.set_attribute("function.kwargs_count", len(kwargs))
|
306
|
+
|
307
|
+
start_time = time.time()
|
308
|
+
|
309
|
+
try:
|
310
|
+
self.debug(f"Starting function execution: {func.__name__}")
|
311
|
+
|
312
|
+
result = func(*args, **kwargs)
|
313
|
+
duration_ms = (time.time() - start_time) * 1000
|
314
|
+
|
315
|
+
# Mark span as successful
|
316
|
+
span.set_attribute("function.duration_ms", duration_ms)
|
317
|
+
span.set_attribute("function.success", True)
|
318
|
+
span.set_status(Status(StatusCode.OK))
|
319
|
+
|
320
|
+
if log_result and result is not None:
|
321
|
+
span.set_attribute("function.has_result", True)
|
322
|
+
span.set_attribute(
|
323
|
+
"function.result_type", type(result).__name__
|
324
|
+
)
|
325
|
+
|
326
|
+
if log_execution:
|
327
|
+
self.log_function_execution(
|
328
|
+
func.__name__,
|
329
|
+
duration_ms,
|
330
|
+
True,
|
331
|
+
{
|
332
|
+
"args_count": len(args) if args else 0,
|
333
|
+
"kwargs_count": len(kwargs) if kwargs else 0,
|
334
|
+
},
|
335
|
+
)
|
336
|
+
|
337
|
+
self.debug(f"Function execution completed: {func.__name__}")
|
338
|
+
return result
|
339
|
+
|
340
|
+
except Exception as e:
|
341
|
+
duration_ms = (time.time() - start_time) * 1000
|
342
|
+
|
343
|
+
# Mark span as failed
|
344
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
345
|
+
span.record_exception(e)
|
346
|
+
span.set_attribute("function.duration_ms", duration_ms)
|
347
|
+
span.set_attribute("function.success", False)
|
348
|
+
span.set_attribute("error.type", type(e).__name__)
|
349
|
+
span.set_attribute("error.message", str(e))
|
350
|
+
|
351
|
+
if log_execution:
|
352
|
+
self.log_function_execution(
|
353
|
+
func.__name__,
|
354
|
+
duration_ms,
|
355
|
+
False,
|
356
|
+
{
|
357
|
+
"error_type": type(e).__name__,
|
358
|
+
"error_message": str(e),
|
359
|
+
},
|
360
|
+
)
|
361
|
+
|
362
|
+
self.error(
|
363
|
+
f"Function execution failed: {func.__name__} - {str(e)}"
|
364
|
+
)
|
365
|
+
raise
|
366
|
+
|
367
|
+
return wrapper
|
368
|
+
|
369
|
+
return decorator
|
370
|
+
|
371
|
+
def add_span_attributes(self, attributes: Dict[str, Union[str, int, float, bool]]):
|
372
|
+
"""Add attributes to the current active span"""
|
373
|
+
current_span = trace.get_current_span()
|
374
|
+
if current_span and current_span.is_recording():
|
375
|
+
for key, value in attributes.items():
|
376
|
+
current_span.set_attribute(key, value)
|
377
|
+
|
378
|
+
def add_span_event(self, name: str, attributes: Optional[Dict[str, Any]] = None):
|
379
|
+
"""Add an event to the current active span"""
|
380
|
+
current_span = trace.get_current_span()
|
381
|
+
if current_span and current_span.is_recording():
|
382
|
+
event_attributes = attributes or {}
|
383
|
+
if self._correlation_id:
|
384
|
+
event_attributes["correlation_id"] = self._correlation_id
|
385
|
+
current_span.add_event(name, event_attributes)
|
386
|
+
|
387
|
+
def set_span_status(
|
388
|
+
self, status_code: StatusCode, description: Optional[str] = None
|
389
|
+
):
|
390
|
+
"""Set the status of the current active span"""
|
391
|
+
current_span = trace.get_current_span()
|
392
|
+
if current_span and current_span.is_recording():
|
393
|
+
current_span.set_status(Status(status_code, description))
|
394
|
+
|
395
|
+
def log_with_span(
|
396
|
+
self,
|
397
|
+
span_name: str,
|
398
|
+
message: str,
|
399
|
+
level: int = logging.INFO,
|
400
|
+
extra: Optional[Dict[str, Any]] = None,
|
401
|
+
span_attributes: Optional[Dict[str, Union[str, int, float, bool]]] = None,
|
402
|
+
):
|
403
|
+
"""
|
404
|
+
Log a message within a span context
|
405
|
+
|
406
|
+
Args:
|
407
|
+
span_name: Name of the span
|
408
|
+
message: Log message
|
409
|
+
level: Log level
|
410
|
+
extra: Additional log properties
|
411
|
+
span_attributes: Attributes to add to the span
|
412
|
+
"""
|
413
|
+
with self.tracer.start_as_current_span(span_name) as span:
|
414
|
+
if span_attributes:
|
415
|
+
for key, value in span_attributes.items():
|
416
|
+
span.set_attribute(key, value)
|
417
|
+
|
418
|
+
self.logger.log(level, message, extra=self._enhance_extra(extra))
|
419
|
+
|
420
|
+
def log_dependency(
|
421
|
+
self,
|
422
|
+
dependency_type: str,
|
423
|
+
name: str,
|
424
|
+
command: str,
|
425
|
+
success: bool,
|
426
|
+
duration_ms: float,
|
427
|
+
extra: Optional[Dict[str, Any]] = None,
|
428
|
+
):
|
429
|
+
"""
|
430
|
+
Log external dependency calls (Database, HTTP, etc.)
|
431
|
+
|
432
|
+
Args:
|
433
|
+
dependency_type: Type of dependency (SQL, HTTP, etc.)
|
434
|
+
name: Name/identifier of the dependency
|
435
|
+
command: Command/query executed
|
436
|
+
success: Whether the call was successful
|
437
|
+
duration_ms: Call duration in milliseconds
|
438
|
+
extra: Additional properties
|
439
|
+
"""
|
440
|
+
log_data = {
|
441
|
+
"dependency_type": dependency_type,
|
442
|
+
"dependency_name": name,
|
443
|
+
"dependency_command": command,
|
444
|
+
"dependency_success": success,
|
445
|
+
"duration_ms": duration_ms,
|
446
|
+
"category": "dependency_call",
|
447
|
+
}
|
448
|
+
|
449
|
+
if extra:
|
450
|
+
log_data.update(extra)
|
451
|
+
|
452
|
+
log_level = logging.INFO if success else logging.ERROR
|
453
|
+
status = "SUCCESS" if success else "FAILED"
|
454
|
+
message = f"Dependency call: {dependency_type}:{name} - {duration_ms:.2f}ms - {status}"
|
455
|
+
|
456
|
+
self.logger.log(log_level, message, extra=self._enhance_extra(log_data))
|
457
|
+
|
458
|
+
def flush(self):
|
459
|
+
"""Flush all pending telemetry data"""
|
460
|
+
if self._telemetry_enabled:
|
461
|
+
try:
|
462
|
+
# Force flush any pending telemetry
|
463
|
+
from opentelemetry.sdk.trace import TracerProvider
|
464
|
+
|
465
|
+
tracer_provider = trace.get_tracer_provider()
|
466
|
+
if hasattr(tracer_provider, "force_flush"):
|
467
|
+
tracer_provider.force_flush(timeout_millis=5000)
|
468
|
+
except Exception as e:
|
469
|
+
self.warning(f"Failed to flush telemetry: {e}")
|
470
|
+
|
471
|
+
|
472
|
+
# Factory functions for easy instantiation
|
473
|
+
def create_app_logger(
|
474
|
+
service_name: str,
|
475
|
+
service_version: str = "1.0.0",
|
476
|
+
connection_string: Optional[str] = None,
|
477
|
+
log_level: int = logging.INFO,
|
478
|
+
enable_console_logging: bool = True,
|
479
|
+
custom_resource_attributes: Optional[Dict[str, str]] = None,
|
480
|
+
) -> AzureLogger:
|
481
|
+
"""
|
482
|
+
Factory function to create an AzureLogger instance
|
483
|
+
|
484
|
+
Args:
|
485
|
+
service_name: Name of your service/application
|
486
|
+
service_version: Version of your service
|
487
|
+
connection_string: Application Insights connection string
|
488
|
+
log_level: Logging level
|
489
|
+
enable_console_logging: Enable console output
|
490
|
+
custom_resource_attributes: Additional resource attributes
|
491
|
+
|
492
|
+
Returns:
|
493
|
+
Configured AzureLogger instance
|
494
|
+
"""
|
495
|
+
return AzureLogger(
|
496
|
+
service_name=service_name,
|
497
|
+
service_version=service_version,
|
498
|
+
connection_string=connection_string,
|
499
|
+
log_level=log_level,
|
500
|
+
enable_console_logging=enable_console_logging,
|
501
|
+
custom_resource_attributes=custom_resource_attributes,
|
502
|
+
)
|
503
|
+
|
504
|
+
|
505
|
+
def create_function_logger(
|
506
|
+
function_app_name: str,
|
507
|
+
function_name: str,
|
508
|
+
service_version: str = "1.0.0",
|
509
|
+
connection_string: Optional[str] = None,
|
510
|
+
) -> AzureLogger:
|
511
|
+
"""
|
512
|
+
Factory function specifically for Azure Functions
|
513
|
+
|
514
|
+
Args:
|
515
|
+
function_app_name: Name of the Function App
|
516
|
+
function_name: Name of the specific function
|
517
|
+
service_version: Version of the service
|
518
|
+
connection_string: Application Insights connection string
|
519
|
+
|
520
|
+
Returns:
|
521
|
+
Configured AzureLogger instance for Azure Functions
|
522
|
+
"""
|
523
|
+
custom_attributes = {
|
524
|
+
"azure.function.app": function_app_name,
|
525
|
+
"azure.function.name": function_name,
|
526
|
+
"azure.resource.type": "function",
|
527
|
+
}
|
528
|
+
|
529
|
+
return create_app_logger(
|
530
|
+
service_name=f"{function_app_name}.{function_name}",
|
531
|
+
service_version=service_version,
|
532
|
+
connection_string=connection_string,
|
533
|
+
custom_resource_attributes=custom_attributes,
|
534
|
+
)
|
@@ -0,0 +1,7 @@
|
|
1
|
+
azpaddypy/mgmt/__init__.py,sha256=5-0eZuJMZlCONZNJ5hEvWXfvLIM36mg7FsEVs32bY_A,183
|
2
|
+
azpaddypy/mgmt/logging.py,sha256=bpxGz674_1_Re5LzC1b6vslImeUQXSPTdBfHbt81jI0,19942
|
3
|
+
azpaddypy-0.1.0.dist-info/licenses/LICENSE,sha256=hQ6t0g2QaewGCQICHqTckBFbMVakGmoyTAzDpmEYV4c,1089
|
4
|
+
azpaddypy-0.1.0.dist-info/METADATA,sha256=IYtfH4emxwpYEuSY1vHij7D4ZeLpYlNPvzaOiS6hcaQ,256
|
5
|
+
azpaddypy-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
6
|
+
azpaddypy-0.1.0.dist-info/top_level.txt,sha256=hsDuboDhT61320ML8X479ezSTwT3rrlDWz1_Z45B2cs,10
|
7
|
+
azpaddypy-0.1.0.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Patrik Hartl
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1 @@
|
|
1
|
+
azpaddypy
|