commoncompute 0.1.0__tar.gz → 0.1.2__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.
- {commoncompute-0.1.0 → commoncompute-0.1.2}/PKG-INFO +28 -22
- {commoncompute-0.1.0 → commoncompute-0.1.2}/README.md +27 -21
- {commoncompute-0.1.0 → commoncompute-0.1.2}/commoncompute/__init__.py +8 -15
- {commoncompute-0.1.0 → commoncompute-0.1.2}/commoncompute/_generated_workloads.py +23 -0
- {commoncompute-0.1.0 → commoncompute-0.1.2}/commoncompute/_transport.py +20 -5
- {commoncompute-0.1.0 → commoncompute-0.1.2}/commoncompute/cli.py +9 -2
- {commoncompute-0.1.0 → commoncompute-0.1.2}/commoncompute/client.py +28 -11
- commoncompute-0.1.2/commoncompute/py.typed +0 -0
- {commoncompute-0.1.0 → commoncompute-0.1.2}/commoncompute/tasks.py +7 -2
- {commoncompute-0.1.0 → commoncompute-0.1.2}/pyproject.toml +1 -1
- {commoncompute-0.1.0 → commoncompute-0.1.2}/.gitignore +0 -0
- {commoncompute-0.1.0 → commoncompute-0.1.2}/LICENSE +0 -0
- {commoncompute-0.1.0 → commoncompute-0.1.2}/commoncompute/aws_batch.py +0 -0
- {commoncompute-0.1.0 → commoncompute-0.1.2}/commoncompute/compat/__init__.py +0 -0
- {commoncompute-0.1.0 → commoncompute-0.1.2}/commoncompute/compat/openai.py +0 -0
- {commoncompute-0.1.0 → commoncompute-0.1.2}/commoncompute/connect.py +0 -0
- {commoncompute-0.1.0 → commoncompute-0.1.2}/commoncompute/errors.py +0 -0
- {commoncompute-0.1.0 → commoncompute-0.1.2}/commoncompute/models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: commoncompute
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Official Python SDK for Common Compute — the batch AI bill you shouldn't be paying.
|
|
5
5
|
Project-URL: Homepage, https://commoncompute.ai
|
|
6
6
|
Project-URL: Documentation, https://commoncompute.ai/docs
|
|
@@ -61,7 +61,8 @@ import commoncompute as cc
|
|
|
61
61
|
|
|
62
62
|
client = cc.Client() # CC_API_KEY, or ~/.commoncompute/config
|
|
63
63
|
|
|
64
|
-
job = client.
|
|
64
|
+
job = client.submit("coreml_embed", {"input": ["what is the neural engine?"]},
|
|
65
|
+
model_id="bge-base")
|
|
65
66
|
print(job.job_id, job.locked_price_usd, job.eta_seconds) # price known BEFORE it runs
|
|
66
67
|
|
|
67
68
|
result = client.result(job) # waits, downloads, attaches the receipt
|
|
@@ -69,7 +70,10 @@ print(result.output)
|
|
|
69
70
|
print(result.receipt) # signed proof of what ran, where, for how much
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
Prefer the OpenAI-style surface? `client.embeddings.create(input=[...],
|
|
74
|
+
model="bge-base")` returns vectors synchronously.
|
|
75
|
+
|
|
76
|
+
## Examples — live today
|
|
73
77
|
|
|
74
78
|
### 1. OCR / document extraction — vs AWS Textract
|
|
75
79
|
|
|
@@ -78,38 +82,40 @@ job = client.ocr.extract("https://example.com/contracts.pdf", structured=True)
|
|
|
78
82
|
result = client.result(job) # blocks + bounding boxes as JSON
|
|
79
83
|
```
|
|
80
84
|
|
|
81
|
-
### 2.
|
|
85
|
+
### 2. Embeddings in bulk
|
|
82
86
|
|
|
83
87
|
```python
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
for result in client.submit_many(
|
|
89
|
+
"coreml_embed",
|
|
90
|
+
[{"input": chunked(doc)} for doc in corpus],
|
|
87
91
|
max_concurrent=16,
|
|
88
|
-
)
|
|
92
|
+
):
|
|
93
|
+
save(result.output) # results stream as they complete
|
|
89
94
|
```
|
|
90
95
|
|
|
91
|
-
### 3.
|
|
96
|
+
### 3. Background removal
|
|
92
97
|
|
|
93
98
|
```python
|
|
94
|
-
job = client.
|
|
95
|
-
top = client.result(job).output
|
|
99
|
+
job = client.images.remove_background("product-shot.png")
|
|
96
100
|
```
|
|
97
101
|
|
|
98
|
-
|
|
102
|
+
Also live: speech synthesis, image classification/detection/pose, barcode
|
|
103
|
+
reading, HEIC conversion — plus the generic `client.submit(workload_id,
|
|
104
|
+
payload)` for anything marked **live** in
|
|
105
|
+
[the catalog](https://commoncompute.ai/workloads).
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
job = client.translate(catalog_descriptions, target_lang="de")
|
|
102
|
-
```
|
|
107
|
+
### In preview
|
|
103
108
|
|
|
104
|
-
|
|
109
|
+
`client.translate(...)` and `client.video.transcode(...)` run on preview
|
|
110
|
+
capacity — functional, not yet GA.
|
|
105
111
|
|
|
106
|
-
|
|
107
|
-
job = client.images.remove_background("product-shot.png")
|
|
108
|
-
```
|
|
112
|
+
### Not live yet
|
|
109
113
|
|
|
110
|
-
|
|
111
|
-
`client.
|
|
112
|
-
|
|
114
|
+
`client.transcription.*`, `client.rerank(...)`, `client.images.generate(...)`,
|
|
115
|
+
`client.chat.*`, and `client.build.ios_test(...)` target workloads still
|
|
116
|
+
marked *coming soon*: the API refuses them with a clear
|
|
117
|
+
`workload_not_available` error (HTTP 409) rather than queueing a job that
|
|
118
|
+
won't run. They activate automatically as those lanes go live — watch
|
|
113
119
|
[the catalog](https://commoncompute.ai/workloads).
|
|
114
120
|
|
|
115
121
|
## Price before execution, hard caps, dry runs
|
|
@@ -26,7 +26,8 @@ import commoncompute as cc
|
|
|
26
26
|
|
|
27
27
|
client = cc.Client() # CC_API_KEY, or ~/.commoncompute/config
|
|
28
28
|
|
|
29
|
-
job = client.
|
|
29
|
+
job = client.submit("coreml_embed", {"input": ["what is the neural engine?"]},
|
|
30
|
+
model_id="bge-base")
|
|
30
31
|
print(job.job_id, job.locked_price_usd, job.eta_seconds) # price known BEFORE it runs
|
|
31
32
|
|
|
32
33
|
result = client.result(job) # waits, downloads, attaches the receipt
|
|
@@ -34,7 +35,10 @@ print(result.output)
|
|
|
34
35
|
print(result.receipt) # signed proof of what ran, where, for how much
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
Prefer the OpenAI-style surface? `client.embeddings.create(input=[...],
|
|
39
|
+
model="bge-base")` returns vectors synchronously.
|
|
40
|
+
|
|
41
|
+
## Examples — live today
|
|
38
42
|
|
|
39
43
|
### 1. OCR / document extraction — vs AWS Textract
|
|
40
44
|
|
|
@@ -43,38 +47,40 @@ job = client.ocr.extract("https://example.com/contracts.pdf", structured=True)
|
|
|
43
47
|
result = client.result(job) # blocks + bounding boxes as JSON
|
|
44
48
|
```
|
|
45
49
|
|
|
46
|
-
### 2.
|
|
50
|
+
### 2. Embeddings in bulk
|
|
47
51
|
|
|
48
52
|
```python
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
for result in client.submit_many(
|
|
54
|
+
"coreml_embed",
|
|
55
|
+
[{"input": chunked(doc)} for doc in corpus],
|
|
52
56
|
max_concurrent=16,
|
|
53
|
-
)
|
|
57
|
+
):
|
|
58
|
+
save(result.output) # results stream as they complete
|
|
54
59
|
```
|
|
55
60
|
|
|
56
|
-
### 3.
|
|
61
|
+
### 3. Background removal
|
|
57
62
|
|
|
58
63
|
```python
|
|
59
|
-
job = client.
|
|
60
|
-
top = client.result(job).output
|
|
64
|
+
job = client.images.remove_background("product-shot.png")
|
|
61
65
|
```
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
Also live: speech synthesis, image classification/detection/pose, barcode
|
|
68
|
+
reading, HEIC conversion — plus the generic `client.submit(workload_id,
|
|
69
|
+
payload)` for anything marked **live** in
|
|
70
|
+
[the catalog](https://commoncompute.ai/workloads).
|
|
64
71
|
|
|
65
|
-
|
|
66
|
-
job = client.translate(catalog_descriptions, target_lang="de")
|
|
67
|
-
```
|
|
72
|
+
### In preview
|
|
68
73
|
|
|
69
|
-
|
|
74
|
+
`client.translate(...)` and `client.video.transcode(...)` run on preview
|
|
75
|
+
capacity — functional, not yet GA.
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
job = client.images.remove_background("product-shot.png")
|
|
73
|
-
```
|
|
77
|
+
### Not live yet
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
`client.
|
|
77
|
-
|
|
79
|
+
`client.transcription.*`, `client.rerank(...)`, `client.images.generate(...)`,
|
|
80
|
+
`client.chat.*`, and `client.build.ios_test(...)` target workloads still
|
|
81
|
+
marked *coming soon*: the API refuses them with a clear
|
|
82
|
+
`workload_not_available` error (HTTP 409) rather than queueing a job that
|
|
83
|
+
won't run. They activate automatically as those lanes go live — watch
|
|
78
84
|
[the catalog](https://commoncompute.ai/workloads).
|
|
79
85
|
|
|
80
86
|
## Price before execution, hard caps, dry runs
|
|
@@ -11,33 +11,26 @@ Quickstart::
|
|
|
11
11
|
client = cc.Client(api_key="cc_live_...")
|
|
12
12
|
print(client.balance())
|
|
13
13
|
|
|
14
|
-
#
|
|
15
|
-
reply = client.chat.create(
|
|
16
|
-
model="qwen-2.5-7b-instruct",
|
|
17
|
-
messages=[{"role": "user", "content": "hi"}],
|
|
18
|
-
)
|
|
19
|
-
print(reply["choices"][0]["message"]["content"])
|
|
20
|
-
|
|
21
|
-
# Native job submission
|
|
14
|
+
# Native job submission (embeddings — live today)
|
|
22
15
|
job = client.jobs.submit(
|
|
23
16
|
workload_id="coreml_embed",
|
|
24
|
-
payload={"
|
|
17
|
+
payload={"input": ["hello world"]},
|
|
25
18
|
)
|
|
26
19
|
result = client.jobs.wait(job["id"])
|
|
27
20
|
|
|
28
|
-
|
|
21
|
+
# OpenAI-compatible embeddings
|
|
22
|
+
vectors = client.embeddings.create(input=["hello world"], model="bge-base")
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
print(delta, end="", flush=True)
|
|
24
|
+
`client.chat.*` targets LLM workloads that are still marked *coming soon*
|
|
25
|
+
in the catalog — the API declines them with `workload_not_available`
|
|
26
|
+
(HTTP 409) until they go live. See https://commoncompute.ai/workloads.
|
|
34
27
|
"""
|
|
35
28
|
|
|
36
29
|
# MUST come before any submodule imports — _transport.py reads
|
|
37
30
|
# `commoncompute.__version__` at import time for the User-Agent header,
|
|
38
31
|
# and the previous ordering (`__version__` defined at the bottom) made
|
|
39
32
|
# every clean install of the SDK fail with a circular ImportError.
|
|
40
|
-
__version__ = "0.1.
|
|
33
|
+
__version__ = "0.1.2"
|
|
41
34
|
|
|
42
35
|
from .client import Client, AsyncClient
|
|
43
36
|
from .errors import (
|
|
@@ -25,6 +25,19 @@ KNOWN_WORKLOAD_IDS: frozenset[str] = frozenset({
|
|
|
25
25
|
"vision_face",
|
|
26
26
|
"vision_detect",
|
|
27
27
|
"vision_pose",
|
|
28
|
+
"vision_docseg",
|
|
29
|
+
"vision_rectangles",
|
|
30
|
+
"vision_hand_pose",
|
|
31
|
+
"vision_animal_pose",
|
|
32
|
+
"vision_saliency",
|
|
33
|
+
"vision_animals",
|
|
34
|
+
"vision_classify",
|
|
35
|
+
"vision_person_seg",
|
|
36
|
+
"vision_trajectory",
|
|
37
|
+
"vision_optical_flow",
|
|
38
|
+
"av_compose",
|
|
39
|
+
"pdfkit_ops",
|
|
40
|
+
"createml_train",
|
|
28
41
|
"vision_barcode",
|
|
29
42
|
"vision_segment",
|
|
30
43
|
"coreml_upscale",
|
|
@@ -43,6 +56,16 @@ KNOWN_WORKLOAD_IDS: frozenset[str] = frozenset({
|
|
|
43
56
|
"mlx_finetune",
|
|
44
57
|
"pytorch_mps",
|
|
45
58
|
"blender_metal",
|
|
59
|
+
"speech_transcribe",
|
|
60
|
+
"mlx_vlm",
|
|
61
|
+
"mlx_llm_cluster",
|
|
62
|
+
"foundation_llm",
|
|
63
|
+
"vision_similarity",
|
|
64
|
+
"image_filter",
|
|
65
|
+
"audio_normalize",
|
|
66
|
+
"webkit_test",
|
|
67
|
+
"notarize",
|
|
68
|
+
"coreml_convert",
|
|
46
69
|
"cpu_bench",
|
|
47
70
|
})
|
|
48
71
|
|
|
@@ -27,7 +27,10 @@ from . import __version__
|
|
|
27
27
|
from .errors import CommonComputeError, NetworkError, _from_status
|
|
28
28
|
|
|
29
29
|
DEFAULT_BASE_URL = "https://api.commoncompute.ai"
|
|
30
|
-
|
|
30
|
+
# Above the API's 120s synchronous poll windows (chat/images block server-side
|
|
31
|
+
# up to 120s) — a 60s default made every slow-but-successful sync call time
|
|
32
|
+
# out client-side and retry into an idempotent-replay 202.
|
|
33
|
+
DEFAULT_TIMEOUT = 180.0
|
|
31
34
|
DEFAULT_MAX_RETRIES = 2
|
|
32
35
|
|
|
33
36
|
|
|
@@ -40,8 +43,14 @@ def _key_from_config_files() -> Optional[str]:
|
|
|
40
43
|
"""
|
|
41
44
|
import pathlib
|
|
42
45
|
|
|
46
|
+
base = pathlib.Path(os.environ.get("CC_CONFIG_DIR") or (pathlib.Path.home() / ".commoncompute"))
|
|
43
47
|
candidates = [
|
|
44
|
-
|
|
48
|
+
base / "config",
|
|
49
|
+
# connect()/`cc login` save here (respects CC_CONFIG_DIR) — the
|
|
50
|
+
# transport previously only checked the hard-coded legacy path, so a
|
|
51
|
+
# key saved under a custom CC_CONFIG_DIR was invisible to new
|
|
52
|
+
# processes.
|
|
53
|
+
base / "credentials",
|
|
45
54
|
pathlib.Path.home() / ".config" / "commoncompute" / "credentials",
|
|
46
55
|
]
|
|
47
56
|
for path in candidates:
|
|
@@ -87,10 +96,13 @@ def _retry_delay(attempt: int, response: Optional[httpx.Response]) -> float:
|
|
|
87
96
|
ra = response.headers.get("retry-after")
|
|
88
97
|
if ra:
|
|
89
98
|
try:
|
|
90
|
-
|
|
99
|
+
# Honor the server's window up to 60s — the API sends
|
|
100
|
+
# Retry-After: 60 on 429s, and clamping to 30 guaranteed the
|
|
101
|
+
# retry landed inside the same rate-limit window.
|
|
102
|
+
return min(max(float(ra), 0.0), 60.0)
|
|
91
103
|
except ValueError:
|
|
92
104
|
pass
|
|
93
|
-
# 0.5s, 1.5s (+ jitter)
|
|
105
|
+
# 0.5s, 1.5s (+ jitter).
|
|
94
106
|
return 0.5 * (3**attempt) + random.random() * 0.25
|
|
95
107
|
|
|
96
108
|
|
|
@@ -203,6 +215,7 @@ class SyncTransport(_BaseTransport):
|
|
|
203
215
|
data: Any = None,
|
|
204
216
|
files: Any = None,
|
|
205
217
|
headers: Optional[Mapping[str, str]] = None,
|
|
218
|
+
raw: bool = False,
|
|
206
219
|
) -> Any:
|
|
207
220
|
hdrs = _build_headers(self._api_key, self._org, headers)
|
|
208
221
|
# Fresh Idempotency-Key per logical call makes retried POSTs replay
|
|
@@ -233,7 +246,9 @@ class SyncTransport(_BaseTransport):
|
|
|
233
246
|
time.sleep(_retry_delay(attempt, resp))
|
|
234
247
|
continue
|
|
235
248
|
_raise_for(resp)
|
|
236
|
-
|
|
249
|
+
# raw=True hands back the httpx.Response so binary bodies
|
|
250
|
+
# (media results) aren't UTF-8-mangled by the text fallback.
|
|
251
|
+
return resp if raw else _parse_body(resp)
|
|
237
252
|
if isinstance(last_exc, httpx.TransportError):
|
|
238
253
|
raise NetworkError(str(last_exc), code="network_error") from last_exc
|
|
239
254
|
raise last_exc if last_exc else CommonComputeError("request failed", status_code=None, code="request_failed")
|
|
@@ -431,7 +431,7 @@ def keys_revoke(key_id: str) -> None:
|
|
|
431
431
|
@app.command()
|
|
432
432
|
def chat(
|
|
433
433
|
prompt: str = typer.Argument(..., help="Your message."),
|
|
434
|
-
model: str = typer.Option("qwen-2.5-7b
|
|
434
|
+
model: str = typer.Option("qwen-2.5-7b", "--model", "-m"),
|
|
435
435
|
stream: bool = typer.Option(True, "--stream/--no-stream"),
|
|
436
436
|
) -> None:
|
|
437
437
|
"""Send a one-shot chat message to a model."""
|
|
@@ -439,7 +439,14 @@ def chat(
|
|
|
439
439
|
try:
|
|
440
440
|
if stream:
|
|
441
441
|
for chunk in c.chat.stream(model=model, messages=[{"role": "user", "content": prompt}]):
|
|
442
|
-
|
|
442
|
+
choice = chunk.get("choices", [{}])[0]
|
|
443
|
+
# delta-shaped per the OpenAI stream contract; fall back to a
|
|
444
|
+
# message-shaped chunk (older deployments sent those).
|
|
445
|
+
delta = (
|
|
446
|
+
choice.get("delta", {}).get("content", "")
|
|
447
|
+
or choice.get("message", {}).get("content", "")
|
|
448
|
+
or ""
|
|
449
|
+
)
|
|
443
450
|
sys.stdout.write(delta)
|
|
444
451
|
sys.stdout.flush()
|
|
445
452
|
sys.stdout.write("\n")
|
|
@@ -14,7 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any, AsyncIterator, BinaryIO, Iterator, Mapping, Optional, Sequence, Union
|
|
15
15
|
|
|
16
16
|
from ._transport import AsyncTransport, SyncTransport
|
|
17
|
-
from .errors import JobTimeoutError, NotFoundError
|
|
17
|
+
from .errors import CommonComputeError, JobTimeoutError, NotFoundError
|
|
18
18
|
from .models import Job, Quote, Receipt, TaskResult
|
|
19
19
|
from .tasks import (
|
|
20
20
|
OCR,
|
|
@@ -114,19 +114,36 @@ class _JobsSync:
|
|
|
114
114
|
|
|
115
115
|
def download(self, task_id: str, *, dest: Optional[Union[str, Path]] = None) -> Union[bytes, Path]:
|
|
116
116
|
"""Download the job result. Returns bytes, or the path if ``dest`` is given."""
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
# Raw-bytes fetch of GET /v1/jobs/:id/result: the parsed-transport
|
|
118
|
+
# path UTF-8-mangled binary media outputs, and a 202 "Result not
|
|
119
|
+
# ready" body was silently saved to disk as the result.
|
|
120
|
+
resp = self._t.request("GET", f"/v1/jobs/{task_id}/result", raw=True)
|
|
121
|
+
if resp.status_code == 202:
|
|
122
|
+
state = "unknown"
|
|
123
|
+
try:
|
|
124
|
+
state = resp.json().get("state", "unknown")
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
raise CommonComputeError(
|
|
128
|
+
f"Result not ready for job {task_id} (state={state}) — wait() for completion first.",
|
|
129
|
+
status_code=202,
|
|
130
|
+
code="result_not_ready",
|
|
131
|
+
)
|
|
132
|
+
content_type = resp.headers.get("content-type", "")
|
|
133
|
+
blob: bytes
|
|
134
|
+
if "application/json" in content_type:
|
|
135
|
+
try:
|
|
136
|
+
data = resp.json()
|
|
137
|
+
except ValueError:
|
|
138
|
+
data = None
|
|
139
|
+
if isinstance(data, dict) and (data.get("url") or data.get("download_url")):
|
|
140
|
+
# The API may return a pre-signed URL instead of inlined bytes.
|
|
122
141
|
import httpx as _httpx
|
|
123
|
-
blob = _httpx.get(url, timeout=60.0).content
|
|
142
|
+
blob = _httpx.get(data.get("url") or data.get("download_url"), timeout=60.0).content
|
|
124
143
|
else:
|
|
125
|
-
blob =
|
|
126
|
-
elif isinstance(data, (bytes, bytearray)):
|
|
127
|
-
blob = bytes(data)
|
|
144
|
+
blob = resp.content
|
|
128
145
|
else:
|
|
129
|
-
blob =
|
|
146
|
+
blob = resp.content
|
|
130
147
|
if dest is None:
|
|
131
148
|
return blob
|
|
132
149
|
dest_path = Path(dest)
|
|
File without changes
|
|
@@ -230,7 +230,7 @@ class Images(_TaskBase):
|
|
|
230
230
|
self,
|
|
231
231
|
prompt: str,
|
|
232
232
|
*,
|
|
233
|
-
model: str =
|
|
233
|
+
model: Optional[str] = None,
|
|
234
234
|
n: int = 1,
|
|
235
235
|
size: Optional[str] = None,
|
|
236
236
|
max_spend_usd: Optional[float] = None,
|
|
@@ -239,7 +239,12 @@ class Images(_TaskBase):
|
|
|
239
239
|
payload: dict = {"prompt": prompt, "n": n}
|
|
240
240
|
if size:
|
|
241
241
|
payload["size"] = size
|
|
242
|
-
|
|
242
|
+
# model was previously accepted and silently dropped (and its old
|
|
243
|
+
# default "flux-schnell" belongs to sd_mlx, not mlx_image) — forward
|
|
244
|
+
# it so the server can route/validate it.
|
|
245
|
+
return self._run(
|
|
246
|
+
"mlx_image", payload, units=n, model_id=model, max_spend_usd=max_spend_usd, dry_run=dry_run
|
|
247
|
+
)
|
|
243
248
|
|
|
244
249
|
|
|
245
250
|
class Video(_TaskBase):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|