odctl 0.4.4__tar.gz → 0.4.6__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.
Files changed (90) hide show
  1. {odctl-0.4.4 → odctl-0.4.6}/PKG-INFO +2 -2
  2. {odctl-0.4.4 → odctl-0.4.6}/pyproject.toml +1 -1
  3. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/main.py +39 -0
  4. odctl-0.4.6/src/odctl/resources/trino/catalog/clickhouse.properties +17 -0
  5. odctl-0.4.6/tests/test_cli_extended.py +146 -0
  6. odctl-0.4.4/src/odctl/resources/trino/catalog/clickhouse.properties +0 -4
  7. odctl-0.4.4/tests/test_cli_extended.py +0 -70
  8. {odctl-0.4.4 → odctl-0.4.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  9. {odctl-0.4.4 → odctl-0.4.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  10. {odctl-0.4.4 → odctl-0.4.6}/.github/renovate.json +0 -0
  11. {odctl-0.4.4 → odctl-0.4.6}/.github/scripts/smoke.sh +0 -0
  12. {odctl-0.4.4 → odctl-0.4.6}/.github/workflows/build-deps.yml +0 -0
  13. {odctl-0.4.4 → odctl-0.4.6}/.github/workflows/build-platform-images.yml +0 -0
  14. {odctl-0.4.4 → odctl-0.4.6}/.github/workflows/ci.yml +0 -0
  15. {odctl-0.4.4 → odctl-0.4.6}/.github/workflows/e2e-test.yml +0 -0
  16. {odctl-0.4.4 → odctl-0.4.6}/.github/workflows/pipeline.yml +0 -0
  17. {odctl-0.4.4 → odctl-0.4.6}/.github/workflows/publish.yml +0 -0
  18. {odctl-0.4.4 → odctl-0.4.6}/.gitignore +0 -0
  19. {odctl-0.4.4 → odctl-0.4.6}/.pre-commit-config.yaml +0 -0
  20. {odctl-0.4.4 → odctl-0.4.6}/.vscode/settings.json +0 -0
  21. {odctl-0.4.4 → odctl-0.4.6}/LICENSE +0 -0
  22. {odctl-0.4.4 → odctl-0.4.6}/README.md +0 -0
  23. {odctl-0.4.4 → odctl-0.4.6}/image/diagram.png +0 -0
  24. {odctl-0.4.4 → odctl-0.4.6}/image/diagram1.drawio +0 -0
  25. {odctl-0.4.4 → odctl-0.4.6}/image/diagram2.drawio +0 -0
  26. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/__init__.py +0 -0
  27. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/config.py +0 -0
  28. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/docker.py +0 -0
  29. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/planner.py +0 -0
  30. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/registry.py +0 -0
  31. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/clickhouse/config.xml +0 -0
  32. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/clickhouse/init.sql +0 -0
  33. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/clickhouse/keeper_config.xml +0 -0
  34. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/clickhouse/macros-11.xml +0 -0
  35. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/clickhouse/macros-12.xml +0 -0
  36. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/clickhouse/macros-21.xml +0 -0
  37. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/clickhouse/macros-22.xml +0 -0
  38. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/clickhouse/users.xml +0 -0
  39. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/compose-analytics.yml +0 -0
  40. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/compose-deps.yml +0 -0
  41. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/compose-flink.yml +0 -0
  42. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/compose-infra.yml +0 -0
  43. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/compose-kafka.yml +0 -0
  44. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/compose-metadata.yml +0 -0
  45. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/compose-mlops.yml +0 -0
  46. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/compose-obsv.yml +0 -0
  47. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/compose-orch.yml +0 -0
  48. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/compose-spark.yml +0 -0
  49. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/compose-store.yml +0 -0
  50. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/deps/Dockerfile +0 -0
  51. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/deps/build.gradle +0 -0
  52. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/deps/build_connectors.sh +0 -0
  53. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/deps/fetch_flink_dependencies.sh +0 -0
  54. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/deps/fetch_spark_dependencies.sh +0 -0
  55. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/deps/fetch_standalone_plugins.sh +0 -0
  56. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/deps/versions.env +0 -0
  57. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/docker/airflow/Dockerfile +0 -0
  58. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/docker/ray-mlops/Dockerfile +0 -0
  59. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/docker/spark/Dockerfile +0 -0
  60. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/flink/flink-conf.yml +0 -0
  61. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/grafana/prometheus.yml +0 -0
  62. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/marquez/marquez.yml +0 -0
  63. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/mlops/serving/lightgbm_inference.py.example +0 -0
  64. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/mlops/serving/vector_search.py.example +0 -0
  65. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/postgres/01-init-databases.sh +0 -0
  66. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/prometheus/alertmanager.yml +0 -0
  67. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/prometheus/prometheus.yml +0 -0
  68. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/registry.yml +0 -0
  69. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/seaweed/s3.json +0 -0
  70. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/spark/log4j2.properties +0 -0
  71. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/spark/spark-defaults.conf +0 -0
  72. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/trino/access-control.properties +0 -0
  73. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/trino/catalog/iceberg.properties +0 -0
  74. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/trino/catalog/kafka.properties +0 -0
  75. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/trino/catalog/postgres.properties +0 -0
  76. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/trino/catalog/valkey.properties +0 -0
  77. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/trino/group-provider.properties +0 -0
  78. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/trino/groups.txt +0 -0
  79. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/resources/trino/rules.json +0 -0
  80. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/ui.py +0 -0
  81. {odctl-0.4.4 → odctl-0.4.6}/src/odctl/workspace.py +0 -0
  82. {odctl-0.4.4 → odctl-0.4.6}/tests/conftest.py +0 -0
  83. {odctl-0.4.4 → odctl-0.4.6}/tests/test_cli.py +0 -0
  84. {odctl-0.4.4 → odctl-0.4.6}/tests/test_compose_profiles.py +0 -0
  85. {odctl-0.4.4 → odctl-0.4.6}/tests/test_config.py +0 -0
  86. {odctl-0.4.4 → odctl-0.4.6}/tests/test_docker.py +0 -0
  87. {odctl-0.4.4 → odctl-0.4.6}/tests/test_planner.py +0 -0
  88. {odctl-0.4.4 → odctl-0.4.6}/tests/test_registry.py +0 -0
  89. {odctl-0.4.4 → odctl-0.4.6}/tests/test_workspace.py +0 -0
  90. {odctl-0.4.4 → odctl-0.4.6}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: odctl
3
- Version: 0.4.4
3
+ Version: 0.4.6
4
4
  Summary: Orchestrator for the Open Data Stack
5
5
  Project-URL: Homepage, https://github.com/jaehyeon-kim/odctl
6
6
  Project-URL: Repository, https://github.com/jaehyeon-kim/odctl
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "odctl"
3
- version = "0.4.4"
3
+ version = "0.4.6"
4
4
  description = "Orchestrator for the Open Data Stack"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -305,6 +305,31 @@ def up(
305
305
  ui.print_success("All requested profiles successfully started!")
306
306
 
307
307
 
308
+ _DEPS_FILE = "compose-deps.yml"
309
+
310
+
311
+ def _shared_deps_are_idle(torn_down: dict) -> bool:
312
+ """Report whether the shared deps project is safe to remove.
313
+
314
+ The deps project owns the ``odctl`` network and the shared JAR volume, so it
315
+ may only go once no other profile has a container left. Containers are listed
316
+ including exited ones, so a stopped-but-present service still counts as in
317
+ use, and deps itself is excluded because its init container always exits.
318
+
319
+ Args:
320
+ torn_down (dict): The plan just stopped, so its files are not consulted.
321
+
322
+ Returns:
323
+ bool: True when nothing outside deps remains.
324
+ """
325
+ if _DEPS_FILE in torn_down:
326
+ return False
327
+
328
+ declared = build_execution_plan(None, True, resolve_deps=False)
329
+ others = {file: profs for file, profs in declared.items() if file != _DEPS_FILE}
330
+ return not get_managed_containers(others)
331
+
332
+
308
333
  @app.command(
309
334
  rich_help_panel="Cluster Lifecycle",
310
335
  epilog="""
@@ -395,6 +420,20 @@ def down(
395
420
  except Exception as e:
396
421
  ui.print_error(f"Failed to stop {file}", details=str(e))
397
422
  raise typer.Exit(1)
423
+
424
+ # `--all` already carries deps in its plan. A named teardown does not, so
425
+ # `odctl down kafka-lite -v` used to leave the init container, the odctl
426
+ # network and the shared JAR volume behind: a wipe that did not wipe. Remove
427
+ # them here, but only once nothing else is running, since every other profile
428
+ # attaches to that network and mounts that volume.
429
+ if volumes and not all and _shared_deps_are_idle(plan):
430
+ ui.print_info("🛑 Stopping [cyan]deps[/cyan]...")
431
+ try:
432
+ stop_stack(_DEPS_FILE, ["deps"], remove_volumes=True)
433
+ except Exception as e:
434
+ ui.print_error("Failed to stop deps", details=str(e))
435
+ raise typer.Exit(1)
436
+
398
437
  ui.print_success("Teardown complete.")
399
438
 
400
439
 
@@ -0,0 +1,17 @@
1
+ connector.name=clickhouse
2
+ # compress=false is what keeps this catalog usable, not a tuning knob. The
3
+ # clickhouse-jdbc 0.7.1 driver shaded into Trino 480 asks for a compressed
4
+ # response two ways at once: the compress=1 URL parameter, which selects
5
+ # ClickHouse's native compressed-block framing, and an Accept-Encoding: lz4
6
+ # header. ClickHouse only honoured the header when enable_http_compression was
7
+ # turned on, and that setting now defaults to 1, so the server wraps the native
8
+ # blocks in an LZ4 frame and answers Content-Encoding: lz4. The driver never
9
+ # unwraps that frame: it reads the frame's bytes as a block header and dies with
10
+ # "Magic is not correct - expect [-126] but got [123]", which is 0x82 expected
11
+ # against the '{' it found inside the frame. Every query through the catalog
12
+ # fails, listing schemas included. Setting compress=false drops both the
13
+ # parameter and the header, so the body arrives as plain RowBinary. The response
14
+ # is uncompressed over the container network, which costs nothing here.
15
+ connection-url=jdbc:clickhouse://ch-11:8123/feature_store?compress=false
16
+ connection-user=user
17
+ connection-password=password
@@ -0,0 +1,146 @@
1
+ from unittest.mock import MagicMock
2
+ from typer.testing import CliRunner
3
+ from odctl.main import app
4
+
5
+ runner = CliRunner()
6
+
7
+
8
+ def test_down_command(monkeypatch):
9
+ mock_stop = MagicMock()
10
+ monkeypatch.setattr("odctl.main.stop_stack", mock_stop)
11
+ monkeypatch.setattr("odctl.main.is_docker_running", lambda: True)
12
+
13
+ # Mock get_managed_containers to return a fake container so down --all proceeds
14
+ mock_container = MagicMock()
15
+ mock_container.config.labels = {"com.docker.compose.service": "kafka"}
16
+ monkeypatch.setattr(
17
+ "odctl.docker.get_managed_containers", lambda x: [mock_container]
18
+ )
19
+ monkeypatch.setattr(
20
+ "odctl.docker.get_stack_details",
21
+ lambda x, y: (["kafka"], [], [], []),
22
+ )
23
+
24
+ result = runner.invoke(app, ["down", "--all"])
25
+ assert result.exit_code == 0
26
+ mock_stop.assert_called()
27
+
28
+
29
+ def test_restart_command(monkeypatch):
30
+ mock_restart = MagicMock()
31
+ monkeypatch.setattr("odctl.main.restart_managed_containers", mock_restart)
32
+ monkeypatch.setattr("odctl.main.is_docker_running", lambda: True)
33
+
34
+ result = runner.invoke(app, ["restart", "kafka-lite"])
35
+ assert result.exit_code == 0
36
+ mock_restart.assert_called()
37
+
38
+
39
+ def test_logs_command(monkeypatch):
40
+ mock_logs = MagicMock()
41
+ monkeypatch.setattr("odctl.main.get_managed_logs", mock_logs)
42
+ monkeypatch.setattr("odctl.main.is_docker_running", lambda: True)
43
+
44
+ result = runner.invoke(app, ["logs", "kafka-lite"])
45
+ assert result.exit_code == 0
46
+ mock_logs.assert_called()
47
+
48
+
49
+ def test_list_command(monkeypatch):
50
+ result = runner.invoke(app, ["list"])
51
+ assert result.exit_code == 0
52
+
53
+
54
+ def test_explain_command(monkeypatch):
55
+ result = runner.invoke(app, ["explain", "kafka-lite"])
56
+ assert result.exit_code == 0
57
+
58
+
59
+ def test_docker_not_running(monkeypatch):
60
+ monkeypatch.setattr("odctl.main.is_docker_running", lambda: False)
61
+ result = runner.invoke(app, ["up", "kafka-lite"])
62
+ assert result.exit_code != 0
63
+ assert "Docker is not reachable" in result.stdout
64
+
65
+
66
+ def test_invalid_profile(monkeypatch):
67
+ monkeypatch.setattr("odctl.main.is_docker_running", lambda: True)
68
+ result = runner.invoke(app, ["up", "invalid_profile"])
69
+ assert result.exit_code != 0
70
+ assert "Unknown profile" in result.stdout
71
+
72
+
73
+ def _stopped_files(mock_stop):
74
+ """The compose files a mocked stop_stack was asked to tear down, in order."""
75
+ return [call.args[0] for call in mock_stop.call_args_list]
76
+
77
+
78
+ def test_down_named_profile_with_volumes_also_removes_shared_deps(monkeypatch):
79
+ """`odctl down kafka-lite -v` must not leave the deps init container, the
80
+ odctl network and the shared JAR volume behind."""
81
+ mock_stop = MagicMock()
82
+ monkeypatch.setattr("odctl.main.stop_stack", mock_stop)
83
+ monkeypatch.setattr("odctl.main.is_docker_running", lambda: True)
84
+ # Nothing else is left running, so deps is safe to remove.
85
+ monkeypatch.setattr("odctl.main.get_managed_containers", lambda plan: [])
86
+
87
+ result = runner.invoke(app, ["down", "kafka-lite", "-v"])
88
+
89
+ assert result.exit_code == 0
90
+ files = _stopped_files(mock_stop)
91
+ assert "compose-deps.yml" in files, files
92
+ deps_call = mock_stop.call_args_list[-1]
93
+ assert deps_call.args[0] == "compose-deps.yml"
94
+ assert deps_call.args[1] == ["deps"]
95
+ assert deps_call.kwargs["remove_volumes"] is True
96
+
97
+
98
+ def test_down_named_profile_keeps_shared_deps_while_others_run(monkeypatch):
99
+ """Another profile still holding containers needs the network and the volume,
100
+ so deps must survive."""
101
+ mock_stop = MagicMock()
102
+ monkeypatch.setattr("odctl.main.stop_stack", mock_stop)
103
+ monkeypatch.setattr("odctl.main.is_docker_running", lambda: True)
104
+ monkeypatch.setattr("odctl.main.get_managed_containers", lambda plan: [MagicMock()])
105
+
106
+ result = runner.invoke(app, ["down", "kafka-lite", "-v"])
107
+
108
+ assert result.exit_code == 0
109
+ assert "compose-deps.yml" not in _stopped_files(mock_stop)
110
+
111
+
112
+ def test_down_named_profile_without_volumes_keeps_shared_deps(monkeypatch):
113
+ """Without -v the data is meant to survive, and so is the shared volume."""
114
+ mock_stop = MagicMock()
115
+ monkeypatch.setattr("odctl.main.stop_stack", mock_stop)
116
+ monkeypatch.setattr("odctl.main.is_docker_running", lambda: True)
117
+ monkeypatch.setattr("odctl.main.get_managed_containers", lambda plan: [])
118
+
119
+ result = runner.invoke(app, ["down", "kafka-lite"])
120
+
121
+ assert result.exit_code == 0
122
+ assert "compose-deps.yml" not in _stopped_files(mock_stop)
123
+
124
+
125
+ def test_down_all_with_volumes_removes_deps_exactly_once(monkeypatch):
126
+ """`--all` already carries deps in its plan, so the idle check must not add a
127
+ second teardown of the same project."""
128
+ mock_stop = MagicMock()
129
+ monkeypatch.setattr("odctl.main.stop_stack", mock_stop)
130
+ monkeypatch.setattr("odctl.main.is_docker_running", lambda: True)
131
+ monkeypatch.setattr("odctl.main.typer.confirm", lambda *a, **k: True)
132
+
133
+ mock_container = MagicMock()
134
+ mock_container.config.labels = {"com.docker.compose.service": "init-deps"}
135
+ monkeypatch.setattr(
136
+ "odctl.docker.get_managed_containers", lambda plan: [mock_container]
137
+ )
138
+ monkeypatch.setattr(
139
+ "odctl.docker.get_stack_details",
140
+ lambda f, p: (["init-deps"], [], [], ["odctl-shared-deps"]),
141
+ )
142
+
143
+ result = runner.invoke(app, ["down", "--all", "-v"])
144
+
145
+ assert result.exit_code == 0
146
+ assert _stopped_files(mock_stop).count("compose-deps.yml") == 1
@@ -1,4 +0,0 @@
1
- connector.name=clickhouse
2
- connection-url=jdbc:clickhouse://ch-11:8123/feature_store
3
- connection-user=user
4
- connection-password=password
@@ -1,70 +0,0 @@
1
- from unittest.mock import MagicMock
2
- from typer.testing import CliRunner
3
- from odctl.main import app
4
-
5
- runner = CliRunner()
6
-
7
-
8
- def test_down_command(monkeypatch):
9
- mock_stop = MagicMock()
10
- monkeypatch.setattr("odctl.main.stop_stack", mock_stop)
11
- monkeypatch.setattr("odctl.main.is_docker_running", lambda: True)
12
-
13
- # Mock get_managed_containers to return a fake container so down --all proceeds
14
- mock_container = MagicMock()
15
- mock_container.config.labels = {"com.docker.compose.service": "kafka"}
16
- monkeypatch.setattr(
17
- "odctl.docker.get_managed_containers", lambda x: [mock_container]
18
- )
19
- monkeypatch.setattr(
20
- "odctl.docker.get_stack_details",
21
- lambda x, y: (["kafka"], [], [], []),
22
- )
23
-
24
- result = runner.invoke(app, ["down", "--all"])
25
- assert result.exit_code == 0
26
- mock_stop.assert_called()
27
-
28
-
29
- def test_restart_command(monkeypatch):
30
- mock_restart = MagicMock()
31
- monkeypatch.setattr("odctl.main.restart_managed_containers", mock_restart)
32
- monkeypatch.setattr("odctl.main.is_docker_running", lambda: True)
33
-
34
- result = runner.invoke(app, ["restart", "kafka-lite"])
35
- assert result.exit_code == 0
36
- mock_restart.assert_called()
37
-
38
-
39
- def test_logs_command(monkeypatch):
40
- mock_logs = MagicMock()
41
- monkeypatch.setattr("odctl.main.get_managed_logs", mock_logs)
42
- monkeypatch.setattr("odctl.main.is_docker_running", lambda: True)
43
-
44
- result = runner.invoke(app, ["logs", "kafka-lite"])
45
- assert result.exit_code == 0
46
- mock_logs.assert_called()
47
-
48
-
49
- def test_list_command(monkeypatch):
50
- result = runner.invoke(app, ["list"])
51
- assert result.exit_code == 0
52
-
53
-
54
- def test_explain_command(monkeypatch):
55
- result = runner.invoke(app, ["explain", "kafka-lite"])
56
- assert result.exit_code == 0
57
-
58
-
59
- def test_docker_not_running(monkeypatch):
60
- monkeypatch.setattr("odctl.main.is_docker_running", lambda: False)
61
- result = runner.invoke(app, ["up", "kafka-lite"])
62
- assert result.exit_code != 0
63
- assert "Docker is not reachable" in result.stdout
64
-
65
-
66
- def test_invalid_profile(monkeypatch):
67
- monkeypatch.setattr("odctl.main.is_docker_running", lambda: True)
68
- result = runner.invoke(app, ["up", "invalid_profile"])
69
- assert result.exit_code != 0
70
- assert "Unknown profile" in result.stdout
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