oneshot-python 0.20.2__tar.gz → 0.23.1__tar.gz
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.
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/PKG-INFO +2 -2
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/README.md +4 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/oneshot/__init__.py +3 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/oneshot/client.py +30 -1
- oneshot_python-0.23.1/oneshot/physical_mail.py +168 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/pyproject.toml +2 -2
- oneshot_python-0.23.1/tests/test_physical_mail.py +43 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/uv.lock +1 -1
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/.gitignore +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/oneshot/_errors.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/oneshot/_types.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/oneshot/x402.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/__init__.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/test_balance.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/test_budgets.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/test_charge_amount.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/test_compute.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/test_domains.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/test_email_payload.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/test_emergency_error.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/test_max_cost_header.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/test_payment_rejection.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/test_phones_pending.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/test_poll_backoff.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/test_request_id.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/test_tag_receipt_value.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/test_wait_false.py +0 -0
- {oneshot_python-0.20.2 → oneshot_python-0.23.1}/tests/test_x402.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: oneshot-python
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Core Python SDK for the OneShot API — HTTP client with x402 payment handling
|
|
3
|
+
Version: 0.23.1
|
|
4
|
+
Summary: Core Python SDK for the OneShot API — HTTP client with x402 payment handling — 35 tools
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Requires-Dist: eth-account>=0.13.0
|
|
@@ -201,3 +201,7 @@ Every method has an `a*` async mirror (`acompute`, `aget_compute_goal`, …).
|
|
|
201
201
|
## License
|
|
202
202
|
|
|
203
203
|
MIT
|
|
204
|
+
|
|
205
|
+
### Physical mail
|
|
206
|
+
|
|
207
|
+
`client.physical_mail` supports U.S. letters and 4×6 postcards through `upload_artwork`, `validate_address`, `preview`, `get_quote`, `approve`, `send`, `get_order`, `recover`, and `cancel`. Async variants prefix the method with `a`. Review each proof and price, then call `approve(..., approved=True)` explicitly. Persist `idempotency_key` before sending. `recover(key)` retrieves the original order without creating another charge. Postal delivery does not prove readership; paid receipts and fulfillment events remain separate. Lob credentials stay on OneShot's server.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""oneshot-python — Core Python SDK for the OneShot API."""
|
|
2
2
|
|
|
3
|
+
from oneshot.physical_mail import PhysicalMail, PostalAddress, MailQuote, MailOrder
|
|
4
|
+
|
|
3
5
|
from oneshot._errors import (
|
|
4
6
|
BudgetExceededError,
|
|
5
7
|
BudgetSyncError,
|
|
@@ -34,6 +36,7 @@ from oneshot.client import OneShotClient
|
|
|
34
36
|
from oneshot.x402 import sign_payment_authorization
|
|
35
37
|
|
|
36
38
|
__all__ = [
|
|
39
|
+
"PhysicalMail", "PostalAddress", "MailQuote", "MailOrder",
|
|
37
40
|
"OneShotClient",
|
|
38
41
|
"OneShotError",
|
|
39
42
|
"ToolError",
|
|
@@ -51,7 +51,7 @@ try:
|
|
|
51
51
|
|
|
52
52
|
SDK_VERSION = _pkg_version("oneshot-python")
|
|
53
53
|
except Exception: # pragma: no cover - editable/source runs without dist metadata
|
|
54
|
-
SDK_VERSION = "0.
|
|
54
|
+
SDK_VERSION = "0.23.1"
|
|
55
55
|
|
|
56
56
|
# ---------------------------------------------------------------------------
|
|
57
57
|
# Environment configuration
|
|
@@ -313,6 +313,11 @@ class OneShotClient:
|
|
|
313
313
|
headers["X-Max-Cost-USDC"] = str(max_cost)
|
|
314
314
|
return headers
|
|
315
315
|
|
|
316
|
+
@property
|
|
317
|
+
def physical_mail(self):
|
|
318
|
+
from oneshot.physical_mail import PhysicalMail
|
|
319
|
+
return PhysicalMail(self)
|
|
320
|
+
|
|
316
321
|
def _read_headers(self) -> dict[str, str]:
|
|
317
322
|
"""Auth headers plus a signed EIP-712 read proof (x-agent-proof) for the
|
|
318
323
|
free READ routes (inbox, sms inbox, notifications, balance, browser
|
|
@@ -1235,6 +1240,30 @@ class OneShotClient:
|
|
|
1235
1240
|
"""Enrich a company from domain, name, LinkedIn URL, or ticker. Async."""
|
|
1236
1241
|
return await self.acall_tool("/v1/tools/enrich/company", kwargs)
|
|
1237
1242
|
|
|
1243
|
+
def local_search(self, **kwargs: Any) -> Any:
|
|
1244
|
+
"""Discover local businesses (restaurants, contractors, practices) by category/keywords x location. Blocking."""
|
|
1245
|
+
return self.call_tool("/v1/tools/local/search", kwargs)
|
|
1246
|
+
|
|
1247
|
+
async def alocal_search(self, **kwargs: Any) -> Any:
|
|
1248
|
+
"""Discover local businesses (restaurants, contractors, practices) by category/keywords x location. Async."""
|
|
1249
|
+
return await self.acall_tool("/v1/tools/local/search", kwargs)
|
|
1250
|
+
|
|
1251
|
+
def local_resolve(self, name: str, **kwargs: Any) -> Any:
|
|
1252
|
+
"""Resolve a business name + one locating field (address/city/postal_code/phone) to its domain, phone, status. Blocking."""
|
|
1253
|
+
return self.call_tool("/v1/tools/local/resolve", {"name": name, **kwargs})
|
|
1254
|
+
|
|
1255
|
+
async def alocal_resolve(self, name: str, **kwargs: Any) -> Any:
|
|
1256
|
+
"""Resolve a business name + one locating field (address/city/postal_code/phone) to its domain, phone, status. Async."""
|
|
1257
|
+
return await self.acall_tool("/v1/tools/local/resolve", {"name": name, **kwargs})
|
|
1258
|
+
|
|
1259
|
+
def gov_solicitations(self, naics: list[str], **kwargs: Any) -> Any:
|
|
1260
|
+
"""Federal Sources Sought / Presolicitation notices (SAM.gov) by NAICS code with the contracting officer's contact. Result carries `data_as_of` (snapshot behind the rows). Blocking."""
|
|
1261
|
+
return self.call_tool("/v1/tools/gov/solicitations", {"naics": naics, **kwargs})
|
|
1262
|
+
|
|
1263
|
+
async def agov_solicitations(self, naics: list[str], **kwargs: Any) -> Any:
|
|
1264
|
+
"""Federal Sources Sought / Presolicitation notices (SAM.gov) by NAICS code with the contracting officer's contact. Result carries `data_as_of` (snapshot behind the rows). Async."""
|
|
1265
|
+
return await self.acall_tool("/v1/tools/gov/solicitations", {"naics": naics, **kwargs})
|
|
1266
|
+
|
|
1238
1267
|
def find_email(self, company_domain: str, *, full_name: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, **kwargs: Any) -> Any:
|
|
1239
1268
|
"""Find a person's email address. Blocking."""
|
|
1240
1269
|
payload: dict[str, Any] = {"company_domain": company_domain, **kwargs}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Physical mail: explicit approval, durable recovery, independent fulfillment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Any, Literal, TypedDict
|
|
4
|
+
import math
|
|
5
|
+
import httpx
|
|
6
|
+
from oneshot.x402 import sign_read_proof, parse_payment_required, sign_payment_authorization, encode_payment_header
|
|
7
|
+
from oneshot._errors import OneShotError
|
|
8
|
+
|
|
9
|
+
class _PostalAddressRequired(TypedDict):
|
|
10
|
+
name: str
|
|
11
|
+
address_line1: str
|
|
12
|
+
address_city: str
|
|
13
|
+
address_state: str
|
|
14
|
+
address_zip: str
|
|
15
|
+
|
|
16
|
+
class PostalAddress(_PostalAddressRequired, total=False):
|
|
17
|
+
address_line2: str
|
|
18
|
+
address_country: Literal['US']
|
|
19
|
+
|
|
20
|
+
class MailQuote(TypedDict):
|
|
21
|
+
quote_id: str
|
|
22
|
+
input_hash: str
|
|
23
|
+
status: Literal['rendering', 'ready']
|
|
24
|
+
total_usdc: str | None
|
|
25
|
+
expires_at: str
|
|
26
|
+
preview: dict[str, Any]
|
|
27
|
+
input: dict[str, Any]
|
|
28
|
+
service_fee_usdc: str | None
|
|
29
|
+
approval_id: str | None
|
|
30
|
+
delivery_proves_readership: Literal[False]
|
|
31
|
+
|
|
32
|
+
class MailOrder(TypedDict):
|
|
33
|
+
order_id: str
|
|
34
|
+
quote_id: str
|
|
35
|
+
receipt_id: str
|
|
36
|
+
order_status: str
|
|
37
|
+
payment_status: str
|
|
38
|
+
fulfillment_status: str
|
|
39
|
+
total_usdc: str
|
|
40
|
+
cancellation_requested: bool
|
|
41
|
+
cancellation_error: str | None
|
|
42
|
+
cancel_before: str | None
|
|
43
|
+
refunded_at: str | None
|
|
44
|
+
signed_receipt: Any
|
|
45
|
+
events: list[dict[str, Any]]
|
|
46
|
+
idempotency_key: str
|
|
47
|
+
delivery_proves_readership: Literal[False]
|
|
48
|
+
|
|
49
|
+
class PhysicalMail:
|
|
50
|
+
def __init__(self, client: Any):
|
|
51
|
+
self.client = client
|
|
52
|
+
|
|
53
|
+
async def _request(self, path: str, method: str = 'POST', body: Any = None, *, mime: str | None = None, key: str | None = None, max_cost: float | None = None) -> Any:
|
|
54
|
+
c = self.client
|
|
55
|
+
scope = 'read' if method == 'GET' else 'submit' if path == '/send' else 'write'
|
|
56
|
+
def headers() -> dict[str, str]:
|
|
57
|
+
h = c._headers(max_cost=max_cost)
|
|
58
|
+
h['x-agent-proof'] = sign_read_proof(c._private_key, c.address, scope=scope)
|
|
59
|
+
if key:
|
|
60
|
+
h['Idempotency-Key'] = key
|
|
61
|
+
if mime:
|
|
62
|
+
h['Content-Type'] = mime
|
|
63
|
+
return h
|
|
64
|
+
if path == '/send':
|
|
65
|
+
await c.aensure_budgets_synced()
|
|
66
|
+
async with httpx.AsyncClient(timeout=60.0) as transport:
|
|
67
|
+
kwargs = {'content': body} if mime else {'json': body}
|
|
68
|
+
url = f'{c.base_url}/v1/tools/physical-mail{path}'
|
|
69
|
+
response = await transport.request(method, url, headers=headers(), **kwargs)
|
|
70
|
+
if response.status_code == 402 and path == '/send':
|
|
71
|
+
parsed = parse_payment_required(response.headers.get('payment-required'))
|
|
72
|
+
accepted = parsed['accepted']
|
|
73
|
+
payment = response.json()['payment_request']
|
|
74
|
+
charge = str(int(accepted['amount']) / 1_000_000)
|
|
75
|
+
c._assert_within_budget(charge)
|
|
76
|
+
if max_cost is not None and float(charge) > max_cost:
|
|
77
|
+
raise OneShotError('Physical mail quote exceeds max_cost')
|
|
78
|
+
auth = sign_payment_authorization(private_key=c._private_key, from_address=c.address,
|
|
79
|
+
to_address=payment['recipient'], amount=charge, token_address=payment['token_address'],
|
|
80
|
+
chain_id=payment['chain_id'], network=accepted['network'], accepted=accepted,
|
|
81
|
+
resource=parsed.get('resource'), extensions=parsed.get('extensions'))
|
|
82
|
+
h = headers() # Proof nonces are single use, including the unpaid leg.
|
|
83
|
+
h['payment-signature'] = encode_payment_header(auth)
|
|
84
|
+
response = await transport.post(url, headers=h, json=body)
|
|
85
|
+
response.raise_for_status()
|
|
86
|
+
return response.json()
|
|
87
|
+
|
|
88
|
+
async def aupload_artwork(self, data: bytes, mime: str) -> dict[str, str]:
|
|
89
|
+
return await self._request('/assets', body=data, mime=mime)
|
|
90
|
+
|
|
91
|
+
async def avalidate_address(self, address: PostalAddress) -> dict[str, Any]:
|
|
92
|
+
return await self._request('/validate-address', body=address)
|
|
93
|
+
|
|
94
|
+
async def apreview(self, mail_input: dict[str, Any]) -> MailQuote:
|
|
95
|
+
return await self._request('/preview', body=mail_input)
|
|
96
|
+
|
|
97
|
+
async def aget_quote(self, quote_id: str) -> MailQuote:
|
|
98
|
+
from urllib.parse import quote
|
|
99
|
+
return await self._request(f'/quotes/{quote(quote_id, safe="")}', 'GET')
|
|
100
|
+
|
|
101
|
+
async def aapprove(self, *, quote_id: str, input_hash: str, total_usdc: str, approved: Literal[True]) -> dict[str, str]:
|
|
102
|
+
if approved is not True:
|
|
103
|
+
raise ValueError('Explicit mailpiece approval required')
|
|
104
|
+
return await self._request('/approve', body=dict(quote_id=quote_id, input_hash=input_hash, total_usdc=total_usdc, approved=True))
|
|
105
|
+
|
|
106
|
+
async def asend(self, *, quote_id: str, approval_id: str, idempotency_key: str, max_cost: float | None = None, memo: str | None = None, decision_context: dict[str, Any] | None = None) -> MailOrder:
|
|
107
|
+
if max_cost is not None and (not math.isfinite(max_cost) or max_cost <= 0):
|
|
108
|
+
raise ValueError('Physical mail max_cost must be finite and positive')
|
|
109
|
+
if not idempotency_key or not approval_id:
|
|
110
|
+
raise ValueError('Persist an idempotency key and explicitly approve the quote before sending')
|
|
111
|
+
body: dict[str, Any] = dict(quote_id=quote_id, approval_id=approval_id)
|
|
112
|
+
if memo is not None:
|
|
113
|
+
body['memo'] = memo
|
|
114
|
+
if decision_context is not None:
|
|
115
|
+
body['decisionContext'] = decision_context
|
|
116
|
+
try:
|
|
117
|
+
return await self._request('/send', body=body, key=idempotency_key, max_cost=max_cost)
|
|
118
|
+
except Exception as error:
|
|
119
|
+
error.idempotency_key = idempotency_key # type: ignore[attr-defined]
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
async def aget_order(self, order_id: str) -> MailOrder:
|
|
123
|
+
from urllib.parse import quote
|
|
124
|
+
return await self._request(f'/orders/{quote(order_id, safe="")}', 'GET')
|
|
125
|
+
|
|
126
|
+
async def arecover(self, key: str) -> MailOrder:
|
|
127
|
+
from urllib.parse import quote
|
|
128
|
+
return await self._request(f'/orders/recover?key={quote(key, safe="")}', 'GET')
|
|
129
|
+
|
|
130
|
+
async def acancel(self, order_id: str) -> MailOrder:
|
|
131
|
+
from urllib.parse import quote
|
|
132
|
+
return await self._request(f'/orders/{quote(order_id, safe="")}/cancel', body={})
|
|
133
|
+
|
|
134
|
+
def upload_artwork(self, *args: Any, **kwargs: Any) -> Any:
|
|
135
|
+
import asyncio
|
|
136
|
+
return asyncio.get_event_loop().run_until_complete(self.aupload_artwork(*args, **kwargs))
|
|
137
|
+
|
|
138
|
+
def validate_address(self, *args: Any, **kwargs: Any) -> Any:
|
|
139
|
+
import asyncio
|
|
140
|
+
return asyncio.get_event_loop().run_until_complete(self.avalidate_address(*args, **kwargs))
|
|
141
|
+
|
|
142
|
+
def preview(self, *args: Any, **kwargs: Any) -> Any:
|
|
143
|
+
import asyncio
|
|
144
|
+
return asyncio.get_event_loop().run_until_complete(self.apreview(*args, **kwargs))
|
|
145
|
+
|
|
146
|
+
def get_quote(self, *args: Any, **kwargs: Any) -> Any:
|
|
147
|
+
import asyncio
|
|
148
|
+
return asyncio.get_event_loop().run_until_complete(self.aget_quote(*args, **kwargs))
|
|
149
|
+
|
|
150
|
+
def approve(self, *args: Any, **kwargs: Any) -> Any:
|
|
151
|
+
import asyncio
|
|
152
|
+
return asyncio.get_event_loop().run_until_complete(self.aapprove(*args, **kwargs))
|
|
153
|
+
|
|
154
|
+
def send(self, *args: Any, **kwargs: Any) -> Any:
|
|
155
|
+
import asyncio
|
|
156
|
+
return asyncio.get_event_loop().run_until_complete(self.asend(*args, **kwargs))
|
|
157
|
+
|
|
158
|
+
def get_order(self, *args: Any, **kwargs: Any) -> Any:
|
|
159
|
+
import asyncio
|
|
160
|
+
return asyncio.get_event_loop().run_until_complete(self.aget_order(*args, **kwargs))
|
|
161
|
+
|
|
162
|
+
def recover(self, *args: Any, **kwargs: Any) -> Any:
|
|
163
|
+
import asyncio
|
|
164
|
+
return asyncio.get_event_loop().run_until_complete(self.arecover(*args, **kwargs))
|
|
165
|
+
|
|
166
|
+
def cancel(self, *args: Any, **kwargs: Any) -> Any:
|
|
167
|
+
import asyncio
|
|
168
|
+
return asyncio.get_event_loop().run_until_complete(self.acancel(*args, **kwargs))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "oneshot-python"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "Core Python SDK for the OneShot API — HTTP client with x402 payment handling"
|
|
3
|
+
version = "0.23.1"
|
|
4
|
+
description = "Core Python SDK for the OneShot API — HTTP client with x402 payment handling — 35 tools"
|
|
5
5
|
readme = {text = "Core Python SDK for the OneShot API", content-type = "text/plain"}
|
|
6
6
|
license = "MIT"
|
|
7
7
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
3
|
+
from oneshot.physical_mail import PhysicalMail
|
|
4
|
+
|
|
5
|
+
@pytest.mark.asyncio
|
|
6
|
+
async def test_preview_and_approval_do_not_send():
|
|
7
|
+
mail = PhysicalMail(MagicMock())
|
|
8
|
+
mail._request = AsyncMock(return_value={})
|
|
9
|
+
await mail.apreview({'artwork': {'kind': 'letter', 'file': 'asset'}})
|
|
10
|
+
assert mail._request.call_args.args[0] == '/preview'
|
|
11
|
+
with pytest.raises(ValueError):
|
|
12
|
+
await mail.aapprove(quote_id='q', input_hash='h', total_usdc='1.05', approved=False)
|
|
13
|
+
await mail.aapprove(quote_id='q', input_hash='h', total_usdc='1.05', approved=True)
|
|
14
|
+
assert mail._request.call_args.args[0] == '/approve'
|
|
15
|
+
|
|
16
|
+
@pytest.mark.asyncio
|
|
17
|
+
async def test_send_preserves_key_and_approval_and_recovery_does_not_send():
|
|
18
|
+
mail = PhysicalMail(MagicMock())
|
|
19
|
+
mail._request = AsyncMock(return_value={})
|
|
20
|
+
with pytest.raises(ValueError):
|
|
21
|
+
await mail.asend(quote_id='q', approval_id='a', idempotency_key='')
|
|
22
|
+
await mail.asend(quote_id='q', approval_id='a', idempotency_key='durable', max_cost=1.05)
|
|
23
|
+
assert mail._request.call_args.kwargs['key'] == 'durable'
|
|
24
|
+
assert mail._request.call_args.kwargs['body'] == {'quote_id': 'q', 'approval_id': 'a'}
|
|
25
|
+
await mail.arecover('a/b')
|
|
26
|
+
assert mail._request.call_args.args == ('/orders/recover?key=a%2Fb', 'GET')
|
|
27
|
+
|
|
28
|
+
@pytest.mark.asyncio
|
|
29
|
+
async def test_transport_error_carries_recovery_key():
|
|
30
|
+
mail = PhysicalMail(MagicMock())
|
|
31
|
+
mail._request = AsyncMock(side_effect=TimeoutError())
|
|
32
|
+
with pytest.raises(TimeoutError) as caught:
|
|
33
|
+
await mail.asend(quote_id='q', approval_id='a', idempotency_key='persisted')
|
|
34
|
+
assert caught.value.idempotency_key == 'persisted'
|
|
35
|
+
|
|
36
|
+
@pytest.mark.asyncio
|
|
37
|
+
@pytest.mark.parametrize('max_cost', [0, -1, float('nan'), float('inf')])
|
|
38
|
+
async def test_invalid_caps_never_submit(max_cost):
|
|
39
|
+
mail = PhysicalMail(MagicMock())
|
|
40
|
+
mail._request = AsyncMock()
|
|
41
|
+
with pytest.raises(ValueError, match='finite and positive'):
|
|
42
|
+
await mail.asend(quote_id='q', approval_id='a', idempotency_key='key', max_cost=max_cost)
|
|
43
|
+
mail._request.assert_not_called()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|