spanwire 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.
spanwire/sampler.py ADDED
@@ -0,0 +1,108 @@
1
+ """Trace sampling algorithms for spanwire."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import TYPE_CHECKING, Optional
7
+
8
+ if TYPE_CHECKING:
9
+ from spanwire.core import SpanContext
10
+
11
+
12
+ class Sampler(ABC):
13
+ """Abstract base class for all spanwire samplers."""
14
+
15
+ @abstractmethod
16
+ def should_sample(
17
+ self,
18
+ parent_context: Optional[SpanContext],
19
+ trace_id: str,
20
+ name: str,
21
+ ) -> bool:
22
+ """Determine if a trace should be sampled."""
23
+ pass
24
+
25
+
26
+ class AlwaysOnSampler(Sampler):
27
+ """Samples every trace."""
28
+
29
+ def should_sample(
30
+ self,
31
+ parent_context: Optional[SpanContext],
32
+ trace_id: str,
33
+ name: str,
34
+ ) -> bool:
35
+ return True
36
+
37
+ def __repr__(self) -> str:
38
+ return "AlwaysOnSampler()"
39
+
40
+
41
+ class AlwaysOffSampler(Sampler):
42
+ """Drops every trace."""
43
+
44
+ def should_sample(
45
+ self,
46
+ parent_context: Optional[SpanContext],
47
+ trace_id: str,
48
+ name: str,
49
+ ) -> bool:
50
+ return False
51
+
52
+ def __repr__(self) -> str:
53
+ return "AlwaysOffSampler()"
54
+
55
+
56
+ class RatioSampler(Sampler):
57
+ """Deterministic ratio-based trace sampler using trace_id hashing.
58
+
59
+ Guarantees consistent sampling decisions for the same trace across services.
60
+ """
61
+
62
+ def __init__(self, ratio: float) -> None:
63
+ if not (0.0 <= ratio <= 1.0):
64
+ raise ValueError(f"Ratio must be between 0.0 and 1.0, got {ratio}")
65
+ self.ratio = float(ratio)
66
+ # 32-bit unsigned threshold
67
+ self._threshold = int(self.ratio * 0xFFFFFFFF)
68
+
69
+ def should_sample(
70
+ self,
71
+ parent_context: Optional[SpanContext],
72
+ trace_id: str,
73
+ name: str,
74
+ ) -> bool:
75
+ if self.ratio <= 0.0:
76
+ return False
77
+ if self.ratio >= 1.0:
78
+ return True
79
+ # Extract lower 8 hex characters (32 bits) of the trace_id
80
+ tail = trace_id[-8:] if len(trace_id) >= 8 else trace_id
81
+ try:
82
+ val = int(tail, 16)
83
+ return val <= self._threshold
84
+ except ValueError:
85
+ return False
86
+
87
+ def __repr__(self) -> str:
88
+ return f"RatioSampler(ratio={self.ratio})"
89
+
90
+
91
+ class ParentBasedSampler(Sampler):
92
+ """Honors parent context sampling decision if present; otherwise delegates to root sampler."""
93
+
94
+ def __init__(self, root: Optional[Sampler] = None) -> None:
95
+ self.root: Sampler = root if root is not None else AlwaysOnSampler()
96
+
97
+ def should_sample(
98
+ self,
99
+ parent_context: Optional[SpanContext],
100
+ trace_id: str,
101
+ name: str,
102
+ ) -> bool:
103
+ if parent_context is not None:
104
+ return parent_context.is_sampled
105
+ return self.root.should_sample(parent_context, trace_id, name)
106
+
107
+ def __repr__(self) -> str:
108
+ return f"ParentBasedSampler(root={self.root!r})"
spanwire/w3c.py ADDED
@@ -0,0 +1,160 @@
1
+ """W3C Trace Context and Baggage standard implementation.
2
+
3
+ Compliant with:
4
+ - W3C Trace Context Recommendation (https://www.w3.org/TR/trace-context/)
5
+ - W3C Baggage Recommendation (https://www.w3.org/TR/baggage/)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import urllib.parse
12
+ from typing import Dict, Mapping, Optional, Tuple
13
+
14
+ TRACEPARENT_REGEX = re.compile(
15
+ r"^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(?:-.*)?$"
16
+ )
17
+ INVALID_TRACE_ID = "0" * 32
18
+ INVALID_SPAN_ID = "0" * 16
19
+
20
+
21
+ def is_valid_trace_id(trace_id: str) -> bool:
22
+ """Check if trace_id is a valid 32-character hex string and not all zeros."""
23
+ if not isinstance(trace_id, str) or len(trace_id) != 32:
24
+ return False
25
+ if trace_id == INVALID_TRACE_ID:
26
+ return False
27
+ return bool(re.fullmatch(r"[0-9a-f]{32}", trace_id))
28
+
29
+
30
+ def is_valid_span_id(span_id: str) -> bool:
31
+ """Check if span_id is a valid 16-character hex string and not all zeros."""
32
+ if not isinstance(span_id, str) or len(span_id) != 16:
33
+ return False
34
+ if span_id == INVALID_SPAN_ID:
35
+ return False
36
+ return bool(re.fullmatch(r"[0-9a-f]{16}", span_id))
37
+
38
+
39
+ def format_traceparent(
40
+ trace_id: str,
41
+ span_id: str,
42
+ trace_flags: int = 1,
43
+ version: str = "00",
44
+ ) -> str:
45
+ """Format trace context into a W3C traceparent header string."""
46
+ flags_hex = f"{trace_flags & 0xFF:02x}"
47
+ return f"{version}-{trace_id.lower()}-{span_id.lower()}-{flags_hex}"
48
+
49
+
50
+ def parse_traceparent(header_value: str) -> Optional[Tuple[str, str, int]]:
51
+ """Parse a W3C traceparent header.
52
+
53
+ Returns:
54
+ (trace_id, parent_span_id, trace_flags) or None if malformed/invalid.
55
+ """
56
+ if not header_value or not isinstance(header_value, str):
57
+ return None
58
+
59
+ clean_value = header_value.strip().lower()
60
+ match = TRACEPARENT_REGEX.match(clean_value)
61
+ if not match:
62
+ return None
63
+
64
+ version, trace_id, parent_id, flags_hex = match.groups()
65
+
66
+ # Version 'ff' is explicitly invalid according to W3C spec
67
+ if version == "ff":
68
+ return None
69
+
70
+ # Version 00 cannot have trailing fields
71
+ if version == "00" and len(clean_value) != 55:
72
+ return None
73
+
74
+ # All zeros are forbidden for trace_id and parent_id
75
+ if trace_id == INVALID_TRACE_ID or parent_id == INVALID_SPAN_ID:
76
+ return None
77
+
78
+ try:
79
+ flags = int(flags_hex, 16)
80
+ except ValueError:
81
+ return None
82
+
83
+ return trace_id, parent_id, flags
84
+
85
+
86
+ def format_baggage(baggage: Mapping[str, str]) -> str:
87
+ """Format key-value pairs into a W3C baggage header."""
88
+ if not baggage:
89
+ return ""
90
+ entries = []
91
+ for k, v in baggage.items():
92
+ k_enc = urllib.parse.quote(str(k).strip(), safe="")
93
+ v_enc = urllib.parse.quote(str(v).strip(), safe="")
94
+ if k_enc:
95
+ entries.append(f"{k_enc}={v_enc}")
96
+ return ", ".join(entries)
97
+
98
+
99
+ def parse_baggage(header_value: str) -> Dict[str, str]:
100
+ """Parse a W3C baggage header into a dictionary of key-value pairs."""
101
+ baggage: Dict[str, str] = {}
102
+ if not header_value or not isinstance(header_value, str):
103
+ return baggage
104
+
105
+ for member in header_value.split(","):
106
+ member = member.strip()
107
+ if not member:
108
+ continue
109
+ # Split on semicolon first to ignore properties (e.g. key=val;prop=x)
110
+ parts = member.split(";", 1)
111
+ key_val = parts[0].strip()
112
+ if "=" in key_val:
113
+ k, v = key_val.split("=", 1)
114
+ k = urllib.parse.unquote(k.strip())
115
+ v = urllib.parse.unquote(v.strip())
116
+ if k:
117
+ baggage[k] = v
118
+ return baggage
119
+
120
+
121
+ def extract_w3c_headers(
122
+ headers: Mapping[str, str],
123
+ ) -> Tuple[Optional[Tuple[str, str, int]], Dict[str, str], str]:
124
+ """Extract traceparent, baggage, and tracestate from HTTP headers.
125
+
126
+ Headers lookup is case-insensitive.
127
+ Returns:
128
+ (traceparent_tuple_or_none, baggage_dict, tracestate_str)
129
+ """
130
+ normalized = {k.lower(): v for k, v in headers.items()}
131
+
132
+ traceparent_val = normalized.get("traceparent")
133
+ parsed_tp = parse_traceparent(traceparent_val) if traceparent_val else None
134
+
135
+ baggage_val = normalized.get("baggage", "")
136
+ parsed_baggage = parse_baggage(baggage_val)
137
+
138
+ tracestate = normalized.get("tracestate", "").strip()
139
+
140
+ return parsed_tp, parsed_baggage, tracestate
141
+
142
+
143
+ def inject_w3c_headers(
144
+ trace_id: str,
145
+ span_id: str,
146
+ trace_flags: int = 1,
147
+ baggage: Optional[Mapping[str, str]] = None,
148
+ tracestate: str = "",
149
+ target_headers: Optional[Dict[str, str]] = None,
150
+ ) -> Dict[str, str]:
151
+ """Inject W3C traceparent, baggage, and tracestate into a header dictionary."""
152
+ out = target_headers if target_headers is not None else {}
153
+ out["traceparent"] = format_traceparent(trace_id, span_id, trace_flags)
154
+ if baggage:
155
+ formatted_baggage = format_baggage(baggage)
156
+ if formatted_baggage:
157
+ out["baggage"] = formatted_baggage
158
+ if tracestate:
159
+ out["tracestate"] = tracestate
160
+ return out
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.4
2
+ Name: spanwire
3
+ Version: 0.1.0
4
+ Summary: Zero-dependency micro-tracing, distributed context propagation & breadcrumb telemetry engine for Python.
5
+ Author-email: Hamidreza <h4midrezam@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/mrhamidrezam/spanwire
8
+ Project-URL: Repository, https://github.com/mrhamidrezam/spanwire
9
+ Project-URL: Issues, https://github.com/mrhamidrezam/spanwire/issues
10
+ Keywords: tracing,distributed-tracing,opentelemetry,w3c,traceparent,baggage,breadcrumbs,telemetry,contextvars,zero-dependency,async
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Topic :: System :: Monitoring
21
+ Classifier: Topic :: System :: Logging
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Dynamic: license-file
27
+
28
+ <div align="center">
29
+
30
+ # ⚡ spanwire
31
+
32
+ **Zero-dependency micro-tracing, distributed context propagation & breadcrumb telemetry engine for Python.**
33
+
34
+ [![PyPI Version](https://img.shields.io/pypi/v/spanwire.svg?color=blue)](https://pypi.org/project/spanwire/)
35
+ [![Python Versions](https://img.shields.io/pypi/pyversions/spanwire.svg)](https://pypi.org/project/spanwire/)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
37
+ [![CI](https://github.com/mrhamidrezam/spanwire/actions/workflows/ci.yml/badge.svg)](https://github.com/mrhamidrezam/spanwire/actions/workflows/ci.yml)
38
+ [![Typing: Typed](https://img.shields.io/badge/typing-typed-purple.svg)](https://peps.python.org/pep-0561/)
39
+ [![Dependencies: Zero](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](#)
40
+
41
+ </div>
42
+
43
+ ---
44
+
45
+ ## 💡 Why spanwire?
46
+
47
+ Standard distributed tracing solutions (like OpenTelemetry SDK) pull in dozens of heavy transitive dependencies, incur noticeable startup delays, and add architectural bloat to lightweight microservices, CLI utilities, serverless functions (AWS Lambda, Cloud Run), and async LLM pipelines.
48
+
49
+ **spanwire** delivers enterprise-grade distributed telemetry and error context trails with **zero external dependencies**, utilizing pure Python standard library primitives (`contextvars`, `dataclasses`, `time`, `secrets`).
50
+
51
+ - **100% W3C Trace Context & Baggage Standard**: Interoperable with Datadog, Jaeger, Grafana Tempo, AWS X-Ray, and OpenTelemetry collectors.
52
+ - **Flawless Context Propagation**: Works out of the box across `asyncio` coroutines, background tasks, and OS threads without context leakage.
53
+ - **In-Memory Breadcrumb Trail**: Maintains an efficient circular ring-buffer of recent operational milestones, automatically attached when errors or exceptions occur.
54
+ - **Multiple Exporters**: ANSI-colorized CLI console, NDJSON/JSONLines for log aggregators (Vector, Fluentd, Promtail), or in-memory collectors for unit testing.
55
+ - **Deterministic Sampling**: Ratio-based trace hashing guarantees synchronized sampling decisions across distributed microservice boundaries.
56
+
57
+ ---
58
+
59
+ ## 📐 Architecture & Flow
60
+
61
+ ```mermaid
62
+ sequenceDiagram
63
+ autonumber
64
+ participant Client as API Gateway (Service A)
65
+ participant Wire as W3C HTTP Headers
66
+ participant Worker as Downstream Worker (Service B)
67
+ participant Exporter as Telemetry Exporter
68
+
69
+ Client->>Client: Start Span ("checkout.process")
70
+ Client->>Client: Record Breadcrumb ("User tier: VIP")
71
+ Client->>Wire: Inject W3C traceparent & baggage headers
72
+ Wire->>Worker: Forward HTTP Request
73
+ Worker->>Worker: Extract W3C Context (inherits trace_id)
74
+ Worker->>Worker: Execute child span ("db.persist_order")
75
+ Worker-->>Exporter: Export child span ([OK] 12.4ms)
76
+ Client-->>Exporter: Export parent span ([OK] 45.1ms)
77
+ ```
78
+
79
+ ---
80
+
81
+ ## 🚀 Installation
82
+
83
+ ```bash
84
+ pip install spanwire
85
+ ```
86
+
87
+ *Requires Python 3.10 or higher. No third-party dependencies required.*
88
+
89
+ ---
90
+
91
+ ## ⚡ Quick Start
92
+
93
+ ### 1. Basic Spans and Console Export
94
+
95
+ ```python
96
+ import time
97
+ from spanwire import Tracer, ConsoleExporter
98
+
99
+ tracer = Tracer(
100
+ service_name="payment-service",
101
+ exporter=ConsoleExporter(colored=True)
102
+ )
103
+
104
+ tracer.add_breadcrumb("Validating customer cart", category="cart")
105
+
106
+ with tracer.span("process_payment", attributes={"order.id": "ord_1042", "amount": 89.00}) as span:
107
+ time.sleep(0.02)
108
+
109
+ # Nested child span
110
+ with tracer.span("stripe.charge") as child:
111
+ time.sleep(0.03)
112
+ child.set_attribute("charge.id", "ch_abc123")
113
+ tracer.add_breadcrumb("Payment captured via webhook", category="billing")
114
+ ```
115
+
116
+ Console Output:
117
+ ```text
118
+ ⚡ [OK] stripe.charge (30.12ms) [trace=63d3791c.. span=ff0dba3fa8e11f3c parent=97bc44600e80142d]
119
+ ↳ attributes: service.name='payment-service', charge.id='ch_abc123'
120
+ ↳ breadcrumbs (2):
121
+ - [INFO] cart: Validating customer cart
122
+ - [INFO] billing: Payment captured via webhook
123
+ ⚡ [OK] process_payment (50.84ms) [trace=63d3791c.. span=97bc44600e80142d]
124
+ ↳ attributes: service.name='payment-service', order.id='ord_1042', amount=89.0
125
+ ```
126
+
127
+ ---
128
+
129
+ ### 2. Distributed Tracing over HTTP (W3C Traceparent & Baggage)
130
+
131
+ ```python
132
+ import requests
133
+ from spanwire import Tracer, set_baggage_item
134
+
135
+ tracer = Tracer(service_name="gateway")
136
+
137
+ # Set user context in baggage
138
+ set_baggage_item("user_id", "u_8841")
139
+
140
+ with tracer.span("http_outgoing_call") as span:
141
+ # 1. Inject W3C headers into outbound HTTP request
142
+ headers = {}
143
+ tracer.inject_headers(headers)
144
+ # headers contains:
145
+ # 'traceparent': '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'
146
+ # 'baggage': 'user_id=u_8841'
147
+
148
+ # requests.get("https://worker.internal/api/tasks", headers=headers)
149
+ ```
150
+
151
+ On the receiving downstream service:
152
+
153
+ ```python
154
+ from spanwire import Tracer
155
+
156
+ worker_tracer = Tracer(service_name="worker")
157
+
158
+ def handle_incoming_request(incoming_headers: dict):
159
+ # Extract parent context & baggage from headers
160
+ parent_ctx, baggage, _ = worker_tracer.extract_headers(incoming_headers)
161
+
162
+ # Continue the distributed trace seamlessly
163
+ with worker_tracer.span("worker.handle_job", parent_context=parent_ctx) as span:
164
+ print(f"Propagated trace_id: {span.context.trace_id}")
165
+ print(f"Parent span_id: {span.parent_span_id}")
166
+ print(f"User ID from baggage: {baggage.get('user_id')}")
167
+ ```
168
+
169
+ ---
170
+
171
+ ### 3. Asynchronous Function Decoration & LLM Agent Pipeline
172
+
173
+ ```python
174
+ import asyncio
175
+ from spanwire import Tracer, ConsoleExporter
176
+
177
+ tracer = Tracer(service_name="llm-orchestrator", exporter=ConsoleExporter())
178
+
179
+ @tracer.trace(name="llm.generate_plan")
180
+ async def generate_plan(prompt: str) -> list[str]:
181
+ tracer.add_breadcrumb(f"Generating plan for: {prompt[:30]}", category="llm")
182
+ await asyncio.sleep(0.05)
183
+ return ["search_db", "render_markdown"]
184
+
185
+ async def main():
186
+ with tracer.span("orchestrate_query"):
187
+ plan = await generate_plan("Summarize latest server telemetry")
188
+
189
+ asyncio.run(main())
190
+ ```
191
+
192
+ ---
193
+
194
+ ## 🛠️ Exporters
195
+
196
+ | Exporter | Description | Best Suited For |
197
+ |---|---|---|
198
+ | `ConsoleExporter` | Formatted CLI output with ANSI colors, durations, and breadcrumb trails | Development, debugging, CLI tools |
199
+ | `JsonLinesExporter` | Newline-delimited JSON (NDJSON) output to file or stdout | Vector, Promtail, Datadog Agent, CloudWatch |
200
+ | `InMemoryExporter` | Stores finished spans in a thread-safe list | Pytest unit assertions, custom telemetry queues |
201
+ | `CallbackExporter` | Invokes custom callback function on each completed span | Custom event emission, Webhooks |
202
+
203
+ ---
204
+
205
+ ## 🎯 Samplers
206
+
207
+ - **`AlwaysOnSampler()`**: Traces 100% of operations (default).
208
+ - **`AlwaysOffSampler()`**: Disables tracing overhead completely.
209
+ - **`RatioSampler(ratio=0.1)`**: Probabilistically samples `10%` of traces using deterministic hashing on `trace_id`.
210
+ - **`ParentBasedSampler(root=...)`**: Respects the upstream service's sampling flag, with fallback to root sampler.
211
+
212
+ ---
213
+
214
+ ## 🧪 Running Unit Tests
215
+
216
+ ```bash
217
+ python -m pytest tests -v
218
+ ```
219
+
220
+ All 33 unit tests execute in ~0.15s with zero external test plugins.
221
+
222
+ ---
223
+
224
+ ## 📜 License
225
+
226
+ MIT License © 2026 Hamidreza Moghaddam Kohi. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,14 @@
1
+ spanwire/__init__.py,sha256=t75NacFTB50nG-8oR1SFS_Vquy0AK_PK5qmQQpSXdVU,3395
2
+ spanwire/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
3
+ spanwire/breadcrumbs.py,sha256=2ZtSNTmfcFOfHv2BR5GbeMEC8iFjP-qYse9hSAJiEck,2653
4
+ spanwire/context.py,sha256=98CZk0_ObKjOc2mHJXkheKqJZB-IOzIALRYP3vwoBOE,1927
5
+ spanwire/core.py,sha256=uyIkeGQNDHG4-D9maR9THtYGii1LSvB-q_O3aVHQgmI,14085
6
+ spanwire/exporters.py,sha256=HwTOdyxkkV1Nu70h7iHtrOJycrXLTamstsIIjogmOhE,6211
7
+ spanwire/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ spanwire/sampler.py,sha256=Lbujg_7Q6U1hUme0P5a7WTVR-3hPCsYh-WdEHyZfieQ,2871
9
+ spanwire/w3c.py,sha256=ZU8_2XmtAIuzmTDEm-aDMOp8Mi_EzWwfZEKpSD9GhAY,5022
10
+ spanwire-0.1.0.dist-info/licenses/LICENSE,sha256=SzrgpH9v6YiNHAbiye6l8jqMihAM3_tt_nJkrHS2A6Q,1081
11
+ spanwire-0.1.0.dist-info/METADATA,sha256=izqOqQZWOK4Tv6-3xxOTy5BaO5PwnbAHf5Nf9bVFnNM,8479
12
+ spanwire-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
13
+ spanwire-0.1.0.dist-info/top_level.txt,sha256=1_NlRrHckhHPZjLE-DZnMe3DEjty4MzNzKJk_xe-U5g,9
14
+ spanwire-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hamidreza Moghaddam Kohi
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
+ spanwire