devhelm 0.5.0__tar.gz → 0.6.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.
- {devhelm-0.5.0 → devhelm-0.6.1}/.github/workflows/spec-check.yml +27 -1
- {devhelm-0.5.0 → devhelm-0.6.1}/PKG-INFO +6 -1
- {devhelm-0.5.0 → devhelm-0.6.1}/README.md +5 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/pyproject.toml +1 -1
- {devhelm-0.5.0 → devhelm-0.6.1}/scripts/typegen.sh +2 -1
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/_generated.py +0 -1
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/_http.py +61 -1
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/client.py +12 -1
- {devhelm-0.5.0 → devhelm-0.6.1}/tests/run_sdk.py +14 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/tests/test_http.py +56 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/uv.lock +1 -1
- {devhelm-0.5.0 → devhelm-0.6.1}/.github/workflows/ci.yml +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/.github/workflows/release.yml +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/.gitignore +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/LICENSE +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/Makefile +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/docs/openapi/monitoring-api.json +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/scripts/inject_strict_config.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/scripts/regen-from.sh +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/scripts/release.sh +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/__init__.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/_errors.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/_pagination.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/_validation.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/py.typed +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/__init__.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/alert_channels.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/api_keys.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/dependencies.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/deploy_lock.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/environments.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/forensics.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/incidents.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/monitors.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/notification_policies.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/resource_groups.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/secrets.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/status.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/status_pages.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/tags.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/resources/webhooks.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/src/devhelm/types.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/tests/__init__.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/tests/test_client.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/tests/test_errors.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/tests/test_negative_validation.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/tests/test_schemas.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/tests/test_spec_parity.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/tests/test_typing.py +0 -0
- {devhelm-0.5.0 → devhelm-0.6.1}/tests/test_validation_helpers.py +0 -0
|
@@ -20,19 +20,45 @@ jobs:
|
|
|
20
20
|
- uses: actions/setup-python@v5
|
|
21
21
|
with:
|
|
22
22
|
python-version: '3.13'
|
|
23
|
+
- uses: actions/setup-node@v4
|
|
24
|
+
with:
|
|
25
|
+
node-version: '20'
|
|
23
26
|
|
|
24
27
|
- name: Download latest OpenAPI spec from monorepo
|
|
25
28
|
run: |
|
|
26
29
|
gh api repos/devhelmhq/mono/contents/docs/openapi/monitoring-api.json \
|
|
27
30
|
-H "Accept: application/vnd.github.raw+json" \
|
|
28
|
-
|
|
31
|
+
> docs/openapi/monitoring-api.json
|
|
29
32
|
env:
|
|
30
33
|
GH_TOKEN: ${{ secrets.MONOREPO_DISPATCH_TOKEN }}
|
|
31
34
|
|
|
35
|
+
# Sparse-checkout the shared OpenAPI preprocessor from mono.
|
|
36
|
+
# typegen.sh prefers $OPENAPI_TOOLS → local sibling → npx; without this
|
|
37
|
+
# step, CI falls through to `npx @devhelm/openapi-tools` which is
|
|
38
|
+
# intentionally unpublished (internal-only package).
|
|
39
|
+
- name: Checkout @devhelm/openapi-tools from mono
|
|
40
|
+
uses: actions/checkout@v4
|
|
41
|
+
with:
|
|
42
|
+
repository: devhelmhq/mono
|
|
43
|
+
token: ${{ secrets.MONOREPO_DISPATCH_TOKEN }}
|
|
44
|
+
path: .mono
|
|
45
|
+
sparse-checkout: packages/openapi-tools
|
|
46
|
+
sparse-checkout-cone-mode: false
|
|
47
|
+
|
|
48
|
+
- name: Build @devhelm/openapi-tools
|
|
49
|
+
working-directory: .mono/packages/openapi-tools
|
|
50
|
+
# mono uses pnpm workspaces; we only need this one package's deps here,
|
|
51
|
+
# so install standalone with npm (no lockfile in this subdir).
|
|
52
|
+
run: |
|
|
53
|
+
npm install --no-package-lock --no-audit --no-fund
|
|
54
|
+
npm run build
|
|
55
|
+
|
|
32
56
|
- run: uv sync
|
|
33
57
|
- run: uv run pytest -v
|
|
34
58
|
|
|
35
59
|
- name: Regenerate types from spec
|
|
60
|
+
env:
|
|
61
|
+
OPENAPI_TOOLS: node ${{ github.workspace }}/.mono/packages/openapi-tools/dist/cli.js
|
|
36
62
|
run: ./scripts/typegen.sh
|
|
37
63
|
|
|
38
64
|
- name: Check for type changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devhelm
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: DevHelm SDK for Python — typed client for monitors, incidents, alerting, and more
|
|
5
5
|
Project-URL: Homepage, https://github.com/devhelmhq/sdk-python
|
|
6
6
|
Project-URL: Repository, https://github.com/devhelmhq/sdk-python.git
|
|
@@ -55,6 +55,11 @@ monitor = client.monitors.create({
|
|
|
55
55
|
"config": {"url": "https://api.example.com/health", "method": "GET"},
|
|
56
56
|
"frequencySeconds": 60,
|
|
57
57
|
"regions": ["us-east"],
|
|
58
|
+
# `managedBy` records who reconciles drift on this resource. Use
|
|
59
|
+
# "DASHBOARD" (the default for one-off SDK scripts), "CLI" if the
|
|
60
|
+
# monitor lives in a `devhelm.yml` you re-deploy, or "TERRAFORM"
|
|
61
|
+
# if it lives in `.tf` you re-apply.
|
|
62
|
+
"managedBy": "DASHBOARD",
|
|
58
63
|
})
|
|
59
64
|
|
|
60
65
|
# Get a single monitor
|
|
@@ -31,6 +31,11 @@ monitor = client.monitors.create({
|
|
|
31
31
|
"config": {"url": "https://api.example.com/health", "method": "GET"},
|
|
32
32
|
"frequencySeconds": 60,
|
|
33
33
|
"regions": ["us-east"],
|
|
34
|
+
# `managedBy` records who reconciles drift on this resource. Use
|
|
35
|
+
# "DASHBOARD" (the default for one-off SDK scripts), "CLI" if the
|
|
36
|
+
# monitor lives in a `devhelm.yml` you re-deploy, or "TERRAFORM"
|
|
37
|
+
# if it lives in `.tf` you re-apply.
|
|
38
|
+
"managedBy": "DASHBOARD",
|
|
34
39
|
})
|
|
35
40
|
|
|
36
41
|
# Get a single monitor
|
|
@@ -55,7 +55,8 @@ uv run datamodel-codegen \
|
|
|
55
55
|
--enum-field-as-literal one \
|
|
56
56
|
--use-one-literal-as-default \
|
|
57
57
|
--input-file-type openapi \
|
|
58
|
-
--formatters ruff-format
|
|
58
|
+
--formatters ruff-format \
|
|
59
|
+
--disable-timestamp
|
|
59
60
|
|
|
60
61
|
# Why --enum-field-as-literal=one + --use-one-literal-as-default?
|
|
61
62
|
#
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from importlib.metadata import PackageNotFoundError
|
|
6
|
+
from importlib.metadata import version as _pkg_version
|
|
5
7
|
from typing import Any
|
|
6
8
|
from urllib.parse import quote
|
|
7
9
|
|
|
@@ -17,6 +19,26 @@ from devhelm._errors import (
|
|
|
17
19
|
DEFAULT_BASE_URL = "https://api.devhelm.io"
|
|
18
20
|
DEFAULT_PAGE_SIZE = 200
|
|
19
21
|
|
|
22
|
+
# Default surface identifier sent on every authenticated request. Wrappers
|
|
23
|
+
# (e.g. the MCP server) override this at construction time so the API can
|
|
24
|
+
# attribute usage to the right devtool. See ``DevhelmConfig.surface``.
|
|
25
|
+
DEFAULT_SURFACE = "sdk-py"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _sdk_version() -> str:
|
|
29
|
+
"""Resolve the installed package version once at import time.
|
|
30
|
+
|
|
31
|
+
Uses ``importlib.metadata`` instead of hardcoding so a single source of
|
|
32
|
+
truth (``pyproject.toml``) flows through to the wire telemetry header.
|
|
33
|
+
Falls back to ``"unknown"`` for editable / source-tree installs that
|
|
34
|
+
don't yet have a dist-info directory; the API treats that as "no version
|
|
35
|
+
reported" rather than rejecting the request.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
return _pkg_version("devhelm")
|
|
39
|
+
except PackageNotFoundError:
|
|
40
|
+
return "unknown"
|
|
41
|
+
|
|
20
42
|
|
|
21
43
|
@dataclass(frozen=True)
|
|
22
44
|
class DevhelmConfig:
|
|
@@ -27,6 +49,19 @@ class DevhelmConfig:
|
|
|
27
49
|
org_id: str | None = None
|
|
28
50
|
workspace_id: str | None = None
|
|
29
51
|
timeout: float = 30.0
|
|
52
|
+
# Devtool surface identifier reported to the API for adoption and
|
|
53
|
+
# version-distribution telemetry. Defaults to ``"sdk-py"``; wrappers
|
|
54
|
+
# such as the MCP server pass ``"mcp"`` instead so their traffic is
|
|
55
|
+
# attributed correctly. See https://devhelm.io/telemetry for the full
|
|
56
|
+
# contract and opt-out semantics.
|
|
57
|
+
surface: str = DEFAULT_SURFACE
|
|
58
|
+
# Surface version. Defaults to the installed ``devhelm`` package
|
|
59
|
+
# version; wrappers should pass their own package version.
|
|
60
|
+
surface_version: str | None = None
|
|
61
|
+
# Surface-specific metadata forwarded as ``X-DevHelm-*`` headers (e.g.
|
|
62
|
+
# the MCP server attaches ``mcp_client``). Keys are normalised to
|
|
63
|
+
# lower-kebab-case and mapped onto ``X-DevHelm-<key>`` on the wire.
|
|
64
|
+
surface_metadata: dict[str, str] = field(default_factory=dict)
|
|
30
65
|
|
|
31
66
|
|
|
32
67
|
def _resolve(value: str | None, env_key: str, label: str) -> str:
|
|
@@ -42,6 +77,30 @@ def _resolve_optional(value: str | None, env_key: str, default: str) -> str:
|
|
|
42
77
|
return value or os.environ.get(env_key) or default
|
|
43
78
|
|
|
44
79
|
|
|
80
|
+
def _telemetry_headers(config: DevhelmConfig) -> dict[str, str]:
|
|
81
|
+
"""Build the ``X-DevHelm-Surface*`` headers for one client instance.
|
|
82
|
+
|
|
83
|
+
Returns an empty dict when ``DEVHELM_TELEMETRY=0`` so the API receives
|
|
84
|
+
no surface signal at all. The opt-out is intentionally a single env var
|
|
85
|
+
rather than a constructor flag — users opt out once for the whole
|
|
86
|
+
process, not per call site. See https://devhelm.io/telemetry.
|
|
87
|
+
"""
|
|
88
|
+
if os.environ.get("DEVHELM_TELEMETRY", "").strip() == "0":
|
|
89
|
+
return {}
|
|
90
|
+
headers: dict[str, str] = {
|
|
91
|
+
"X-DevHelm-Surface": config.surface,
|
|
92
|
+
"X-DevHelm-Surface-Version": config.surface_version or _sdk_version(),
|
|
93
|
+
# Always identify the underlying SDK so the API can distinguish
|
|
94
|
+
# "raw SDK call" from "wrapper-on-top-of-SDK call" (the latter
|
|
95
|
+
# overrides ``Surface`` to e.g. ``mcp`` but the SDK fingerprint
|
|
96
|
+
# stays available for debugging client-version skew).
|
|
97
|
+
"X-DevHelm-Sdk-Name": "sdk-py",
|
|
98
|
+
}
|
|
99
|
+
for key, value in config.surface_metadata.items():
|
|
100
|
+
headers[f"X-DevHelm-{key}"] = value
|
|
101
|
+
return headers
|
|
102
|
+
|
|
103
|
+
|
|
45
104
|
def build_client(config: DevhelmConfig) -> httpx.Client:
|
|
46
105
|
"""Create a configured httpx.Client with auth and tenant headers."""
|
|
47
106
|
base_url = config.base_url.rstrip("/")
|
|
@@ -56,6 +115,7 @@ def build_client(config: DevhelmConfig) -> httpx.Client:
|
|
|
56
115
|
"Content-Type": "application/json",
|
|
57
116
|
"x-phelm-org-id": org_id,
|
|
58
117
|
"x-phelm-workspace-id": workspace_id,
|
|
118
|
+
**_telemetry_headers(config),
|
|
59
119
|
},
|
|
60
120
|
timeout=config.timeout,
|
|
61
121
|
)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from devhelm._http import DevhelmConfig, build_client
|
|
5
|
+
from devhelm._http import DEFAULT_SURFACE, DevhelmConfig, build_client
|
|
6
6
|
from devhelm.resources.alert_channels import AlertChannels
|
|
7
7
|
from devhelm.resources.api_keys import ApiKeys
|
|
8
8
|
from devhelm.resources.dependencies import Dependencies
|
|
@@ -62,13 +62,24 @@ class Devhelm:
|
|
|
62
62
|
org_id: str | None = None,
|
|
63
63
|
workspace_id: str | None = None,
|
|
64
64
|
timeout: float = 30.0,
|
|
65
|
+
surface: str | None = None,
|
|
66
|
+
surface_version: str | None = None,
|
|
67
|
+
surface_metadata: dict[str, str] | None = None,
|
|
65
68
|
) -> None:
|
|
69
|
+
# ``surface`` / ``surface_version`` / ``surface_metadata`` are passthroughs
|
|
70
|
+
# for wrappers (e.g. the MCP server) that want their traffic attributed
|
|
71
|
+
# to a different devtool surface than the default ``sdk-py``. End users
|
|
72
|
+
# of the SDK should leave these unset. See
|
|
73
|
+
# https://devhelm.io/telemetry for the wire contract and opt-out.
|
|
66
74
|
config = DevhelmConfig(
|
|
67
75
|
token=token,
|
|
68
76
|
base_url=base_url,
|
|
69
77
|
org_id=org_id,
|
|
70
78
|
workspace_id=workspace_id,
|
|
71
79
|
timeout=timeout,
|
|
80
|
+
surface=surface if surface is not None else DEFAULT_SURFACE,
|
|
81
|
+
surface_version=surface_version,
|
|
82
|
+
surface_metadata=surface_metadata if surface_metadata is not None else {},
|
|
72
83
|
)
|
|
73
84
|
client = build_client(config)
|
|
74
85
|
|
|
@@ -393,6 +393,20 @@ def run(client: Devhelm, resource: str, action: str, rest: list[str]) -> Any: #
|
|
|
393
393
|
client.status_pages.subscribers.remove(rest[0], rest[1])
|
|
394
394
|
return None
|
|
395
395
|
|
|
396
|
+
# -- Forensics --
|
|
397
|
+
if op == "forensics.incident-timeline":
|
|
398
|
+
return client.forensics.incident_timeline(rest[0])
|
|
399
|
+
if op == "forensics.check-trace":
|
|
400
|
+
return client.forensics.check_trace(rest[0])
|
|
401
|
+
if op == "forensics.policy-snapshot":
|
|
402
|
+
return client.forensics.policy_snapshot(rest[0])
|
|
403
|
+
if op == "forensics.monitor-rule-evaluations":
|
|
404
|
+
opts = json.loads(rest[1]) if len(rest) > 1 and rest[1] else {}
|
|
405
|
+
return client.forensics.monitor_rule_evaluations(rest[0], **opts)
|
|
406
|
+
if op == "forensics.monitor-transitions":
|
|
407
|
+
opts = json.loads(rest[1]) if len(rest) > 1 and rest[1] else {}
|
|
408
|
+
return client.forensics.monitor_transitions(rest[0], **opts)
|
|
409
|
+
|
|
396
410
|
# -- Status Page Domains --
|
|
397
411
|
if op == "status-pages.domains.list":
|
|
398
412
|
return client.status_pages.domains.list(rest[0])
|
|
@@ -64,6 +64,62 @@ class TestBuildClient:
|
|
|
64
64
|
client.close()
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
# ---------- Surface telemetry headers ----------
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestSurfaceTelemetry:
|
|
71
|
+
"""The SDK reports its identity to the API on every authenticated request
|
|
72
|
+
so the GTM rollup can attribute usage. See https://devhelm.io/telemetry."""
|
|
73
|
+
|
|
74
|
+
def test_default_headers_announce_sdk_py(
|
|
75
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
76
|
+
) -> None:
|
|
77
|
+
monkeypatch.delenv("DEVHELM_TELEMETRY", raising=False)
|
|
78
|
+
client = build_client(DevhelmConfig(token="t"))
|
|
79
|
+
assert client.headers["x-devhelm-surface"] == "sdk-py"
|
|
80
|
+
# version comes from importlib.metadata; its exact value is the
|
|
81
|
+
# SDK release, but it must always be a non-empty string.
|
|
82
|
+
assert client.headers["x-devhelm-surface-version"]
|
|
83
|
+
assert client.headers["x-devhelm-sdk-name"] == "sdk-py"
|
|
84
|
+
client.close()
|
|
85
|
+
|
|
86
|
+
def test_wrapper_can_override_surface(
|
|
87
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
88
|
+
) -> None:
|
|
89
|
+
monkeypatch.delenv("DEVHELM_TELEMETRY", raising=False)
|
|
90
|
+
client = build_client(
|
|
91
|
+
DevhelmConfig(
|
|
92
|
+
token="t",
|
|
93
|
+
surface="mcp",
|
|
94
|
+
surface_version="0.5.0",
|
|
95
|
+
surface_metadata={"Mcp-Client": "cursor"},
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
assert client.headers["x-devhelm-surface"] == "mcp"
|
|
99
|
+
assert client.headers["x-devhelm-surface-version"] == "0.5.0"
|
|
100
|
+
# SDK identity is preserved alongside the wrapper surface.
|
|
101
|
+
assert client.headers["x-devhelm-sdk-name"] == "sdk-py"
|
|
102
|
+
assert client.headers["x-devhelm-mcp-client"] == "cursor"
|
|
103
|
+
client.close()
|
|
104
|
+
|
|
105
|
+
def test_env_opt_out_drops_all_surface_headers(
|
|
106
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
107
|
+
) -> None:
|
|
108
|
+
monkeypatch.setenv("DEVHELM_TELEMETRY", "0")
|
|
109
|
+
client = build_client(
|
|
110
|
+
DevhelmConfig(token="t", surface="mcp", surface_metadata={"X": "y"})
|
|
111
|
+
)
|
|
112
|
+
# Surface, version, sdk-name, and any extras must all be absent.
|
|
113
|
+
assert "x-devhelm-surface" not in client.headers
|
|
114
|
+
assert "x-devhelm-surface-version" not in client.headers
|
|
115
|
+
assert "x-devhelm-sdk-name" not in client.headers
|
|
116
|
+
assert "x-devhelm-x" not in client.headers
|
|
117
|
+
# Auth + tenant headers must still be there — opt-out is for
|
|
118
|
+
# telemetry only, not for legitimate routing headers.
|
|
119
|
+
assert client.headers["x-phelm-org-id"] == "1"
|
|
120
|
+
client.close()
|
|
121
|
+
|
|
122
|
+
|
|
67
123
|
# ---------- Pydantic validation helpers ----------
|
|
68
124
|
|
|
69
125
|
|
|
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
|
|
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
|