procodile 0.0.9__tar.gz → 0.1.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.
- {procodile-0.0.9 → procodile-0.1.1}/PKG-INFO +4 -4
- {procodile-0.0.9 → procodile-0.1.1}/procodile/__init__.py +2 -3
- {procodile-0.0.9 → procodile-0.1.1}/procodile/artifacts.py +4 -0
- {procodile-0.0.9 → procodile-0.1.1}/procodile/cli/__init__.py +1 -1
- {procodile-0.0.9 → procodile-0.1.1}/procodile/cli/cli.py +1 -1
- {procodile-0.0.9 → procodile-0.1.1}/procodile/job.py +2 -4
- {procodile-0.0.9 → procodile-0.1.1}/procodile/process.py +20 -32
- {procodile-0.0.9 → procodile-0.1.1}/procodile/registry.py +1 -1
- {procodile-0.0.9 → procodile-0.1.1}/procodile/reporter.py +1 -1
- procodile-0.1.1/procodile/schema.py +159 -0
- {procodile-0.0.9 → procodile-0.1.1}/procodile/workflow.py +1 -1
- {procodile-0.0.9 → procodile-0.1.1}/pyproject.toml +7 -5
- {procodile-0.0.9 → procodile-0.1.1}/.gitignore +0 -0
- {procodile-0.0.9 → procodile-0.1.1}/LICENSE +0 -0
- {procodile-0.0.9 → procodile-0.1.1}/README.md +0 -0
- {procodile-0.0.9 → procodile-0.1.1}/procodile/py.typed +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: procodile
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: A light-weight processor development framework
|
|
5
5
|
Project-URL: Documentation, https://eo-tools.github.io/eozilla
|
|
6
6
|
Project-URL: Repository, https://github.com/eo-tools/eozilla
|
|
7
7
|
Project-URL: Changelog, https://github.com/eo-tools/eozilla/blob/main/CHANGES.md
|
|
8
8
|
Project-URL: Issues, https://github.com/eo-tools/eozilla/issues
|
|
9
|
-
Author:
|
|
9
|
+
Author-email: b-yogesh <b-yogesh@users.noreply.github.com>, forman <forman@users.noreply.github.com>, pont-us <pont-us@users.noreply.github.com>, TejasMorbagal <TejasMorbagal@users.noreply.github.com>
|
|
10
10
|
License: Apache-2.0
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Keywords: data processing,eo,esa,ogc
|
|
@@ -19,15 +19,15 @@ Classifier: Operating System :: Microsoft :: Windows
|
|
|
19
19
|
Classifier: Operating System :: POSIX
|
|
20
20
|
Classifier: Operating System :: Unix
|
|
21
21
|
Classifier: Programming Language :: Python :: 3
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
26
|
Classifier: Topic :: Scientific/Engineering
|
|
27
27
|
Classifier: Topic :: Software Development
|
|
28
28
|
Classifier: Typing :: Typed
|
|
29
29
|
Requires-Python: >=3.10
|
|
30
|
-
Requires-Dist: gavicore>=0.
|
|
30
|
+
Requires-Dist: gavicore>=0.1.1
|
|
31
31
|
Requires-Dist: pydantic
|
|
32
32
|
Requires-Dist: pyyaml
|
|
33
33
|
Requires-Dist: typer
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2025 by the Eozilla team and contributors
|
|
1
|
+
# Copyright (c) 2025-2026 by the Eozilla team and contributors
|
|
2
2
|
# Permissions are hereby granted under the terms of the Apache 2.0 License:
|
|
3
3
|
# https://opensource.org/license/apache-2-0.
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ from gavicore.util.request import ExecutionRequest
|
|
|
10
10
|
|
|
11
11
|
from .artifacts import ArtifactRef, ArtifactStore, ExecutionContext
|
|
12
12
|
from .job import Job, JobCancelledException, JobContext
|
|
13
|
-
from .process import Process
|
|
13
|
+
from .process import Process
|
|
14
14
|
from .registry import ProcessRegistry
|
|
15
15
|
from .workflow import (
|
|
16
16
|
FromMain,
|
|
@@ -22,7 +22,6 @@ from .workflow import (
|
|
|
22
22
|
"""Processes development API."""
|
|
23
23
|
|
|
24
24
|
__all__ = [
|
|
25
|
-
"additional_parameters",
|
|
26
25
|
"ArtifactRef",
|
|
27
26
|
"ArtifactStore",
|
|
28
27
|
"__version__",
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# Copyright (c) 2026 by the Eozilla team and contributors
|
|
2
|
+
# Permissions are hereby granted under the terms of the Apache 2.0 License:
|
|
3
|
+
# https://opensource.org/license/apache-2-0.
|
|
4
|
+
|
|
1
5
|
from abc import ABC, abstractmethod
|
|
2
6
|
from dataclasses import dataclass
|
|
3
7
|
from typing import Any, Mapping, TypeAlias
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2025 by the Eozilla team and contributors
|
|
1
|
+
# Copyright (c) 2025-2026 by the Eozilla team and contributors
|
|
2
2
|
# Permissions are hereby granted under the terms of the Apache 2.0 License:
|
|
3
3
|
# https://opensource.org/license/apache-2-0.
|
|
4
4
|
|
|
@@ -17,7 +17,6 @@ from gavicore.models import (
|
|
|
17
17
|
JobInfo,
|
|
18
18
|
JobResults,
|
|
19
19
|
JobStatus,
|
|
20
|
-
JobType,
|
|
21
20
|
ProcessRequest,
|
|
22
21
|
Schema,
|
|
23
22
|
Subscriber,
|
|
@@ -195,9 +194,8 @@ class Job(JobContext):
|
|
|
195
194
|
self.process = process
|
|
196
195
|
# noinspection PyTypeChecker
|
|
197
196
|
self.job_info = JobInfo( # noqa [call-arg]
|
|
198
|
-
type=JobType.process,
|
|
199
|
-
processID=process.description.id,
|
|
200
197
|
jobID=job_id,
|
|
198
|
+
processID=process.description.id,
|
|
201
199
|
status=JobStatus.accepted,
|
|
202
200
|
created=self._now(),
|
|
203
201
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2025 by the Eozilla team and contributors
|
|
1
|
+
# Copyright (c) 2025-2026 by the Eozilla team and contributors
|
|
2
2
|
# Permissions are hereby granted under the terms of the Apache 2.0 License:
|
|
3
3
|
# https://opensource.org/license/apache-2-0.
|
|
4
4
|
|
|
@@ -10,14 +10,13 @@ from pydantic import BaseModel, Field, ValidationError, create_model
|
|
|
10
10
|
from pydantic.fields import FieldInfo
|
|
11
11
|
|
|
12
12
|
from gavicore.models import (
|
|
13
|
-
AdditionalParameter,
|
|
14
|
-
AdditionalParameters,
|
|
15
13
|
InputDescription,
|
|
16
14
|
OutputDescription,
|
|
17
15
|
ProcessDescription,
|
|
18
16
|
Schema,
|
|
19
17
|
)
|
|
20
|
-
|
|
18
|
+
|
|
19
|
+
from .schema import model_class_to_openapi_schema_dict
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
@dataclass
|
|
@@ -133,31 +132,6 @@ class Process:
|
|
|
133
132
|
)
|
|
134
133
|
|
|
135
134
|
|
|
136
|
-
def additional_parameters(
|
|
137
|
-
parameters: dict[str, Any], **metadata: Any
|
|
138
|
-
) -> AdditionalParameters:
|
|
139
|
-
"""
|
|
140
|
-
Helper function that creates an instance of `AdditionalParameters`
|
|
141
|
-
from the keys and values given the `parameters` dictionary.
|
|
142
|
-
The return value is used as the `additionalParameters` argument of an
|
|
143
|
-
`InputDescription` or `OutputDescription`.
|
|
144
|
-
|
|
145
|
-
Args:
|
|
146
|
-
parameters: the parameter key-value pairs.
|
|
147
|
-
metadata: Other metadata fields passed to `AdditionalParameters`.
|
|
148
|
-
|
|
149
|
-
Returns:
|
|
150
|
-
A `AdditionalParameters` instance that can be passed to an
|
|
151
|
-
`InputDescription` or `OutputDescription`.
|
|
152
|
-
"""
|
|
153
|
-
return AdditionalParameters(
|
|
154
|
-
parameters=[
|
|
155
|
-
AdditionalParameter(name=k, value=[v]) for k, v in parameters.items()
|
|
156
|
-
],
|
|
157
|
-
**metadata,
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
|
|
161
135
|
def _parse_inputs(
|
|
162
136
|
fn_name: str,
|
|
163
137
|
signature: inspect.Signature,
|
|
@@ -182,16 +156,20 @@ def _parse_inputs(
|
|
|
182
156
|
model_class = create_model("ProcessInputs", **model_field_definitions)
|
|
183
157
|
|
|
184
158
|
if inputs:
|
|
185
|
-
|
|
159
|
+
model_class_merged = _merge_inputs_into_model_class(
|
|
186
160
|
fn_name=fn_name,
|
|
187
161
|
signature=signature,
|
|
188
162
|
inputs=inputs,
|
|
189
163
|
model_class=model_class,
|
|
190
164
|
)
|
|
165
|
+
# TODO: Work out why mypy dislikes this apparently valid assignment
|
|
166
|
+
model_class = model_class_merged # type:ignore[misc,assignment]
|
|
191
167
|
|
|
192
168
|
model_class.model_rebuild()
|
|
193
169
|
|
|
194
|
-
inputs_schema_dict: dict[str, Any] =
|
|
170
|
+
inputs_schema_dict: dict[str, Any] = model_class_to_openapi_schema_dict(
|
|
171
|
+
model_class, inline_refs=True
|
|
172
|
+
)
|
|
195
173
|
required_names: list[str] = inputs_schema_dict.get("required", [])
|
|
196
174
|
properties: dict[str, Any] = inputs_schema_dict.get("properties", {})
|
|
197
175
|
|
|
@@ -224,7 +202,9 @@ def _parse_outputs(
|
|
|
224
202
|
fn_name, output_annotation, outputs
|
|
225
203
|
)
|
|
226
204
|
model_class = create_model("Outputs", **model_field_definitions) # type: ignore[call-overload]
|
|
227
|
-
outputs_schema_dict =
|
|
205
|
+
outputs_schema_dict = model_class_to_openapi_schema_dict(
|
|
206
|
+
model_class, inline_refs=True
|
|
207
|
+
)
|
|
228
208
|
properties = outputs_schema_dict.get("properties", {})
|
|
229
209
|
|
|
230
210
|
output_descriptions: dict[str, OutputDescription] = {}
|
|
@@ -266,6 +246,12 @@ def _parse_parameters(
|
|
|
266
246
|
return arg_parameters, job_ctx_arg
|
|
267
247
|
|
|
268
248
|
|
|
249
|
+
# TODO: replace deprecated FieldInfo.merge_field_infos()
|
|
250
|
+
# used in _merge_inputs_into_model_class(). See
|
|
251
|
+
# - https://github.com/pydantic/pydantic/issues/12329
|
|
252
|
+
# - https://github.com/pydantic/pydantic/issues/12374
|
|
253
|
+
|
|
254
|
+
|
|
269
255
|
def _merge_inputs_into_model_class(
|
|
270
256
|
fn_name: str,
|
|
271
257
|
signature: inspect.Signature,
|
|
@@ -416,6 +402,7 @@ def _merge_input_descriptions(
|
|
|
416
402
|
additionalParameters=(
|
|
417
403
|
base_description.additionalParameters or description.additionalParameters
|
|
418
404
|
),
|
|
405
|
+
**(base_description.model_extra or {}),
|
|
419
406
|
)
|
|
420
407
|
|
|
421
408
|
|
|
@@ -435,6 +422,7 @@ def _merge_output_descriptions(
|
|
|
435
422
|
additionalParameters=(
|
|
436
423
|
base_description.additionalParameters or description.additionalParameters
|
|
437
424
|
),
|
|
425
|
+
**(base_description.model_extra or {}),
|
|
438
426
|
)
|
|
439
427
|
|
|
440
428
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026 by the Eozilla team and contributors
|
|
2
|
+
# Permissions are hereby granted under the terms of the Apache 2.0 License:
|
|
3
|
+
# https://opensource.org/license/apache-2-0.
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import pydantic
|
|
10
|
+
|
|
11
|
+
from gavicore.util.ensure import ensure_condition
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def model_class_to_openapi_schema_dict(
|
|
15
|
+
model_class: type[pydantic.BaseModel],
|
|
16
|
+
*,
|
|
17
|
+
inline_refs: bool = False,
|
|
18
|
+
openapi_spec: str = "3.0",
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
|
+
"""
|
|
21
|
+
Convert a Pydantic model class to an OpenAPI schema `dict` object.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
model_class: The Pydantic model class.
|
|
25
|
+
inline_refs: Whether to inline any references (`$ref` values)
|
|
26
|
+
generated by `model_class.model_json_schema()`.
|
|
27
|
+
openapi_spec: The OpenAPI specification.
|
|
28
|
+
Currently, only `"3.0"` is supported.
|
|
29
|
+
Returns:
|
|
30
|
+
An OpenAPI schema `dict` object.
|
|
31
|
+
"""
|
|
32
|
+
model_class.model_rebuild()
|
|
33
|
+
schema = model_class.model_json_schema()
|
|
34
|
+
if inline_refs:
|
|
35
|
+
schema = inline_schema_refs(schema)
|
|
36
|
+
return json_schema_to_openapi_schema(schema, openapi_spec=openapi_spec)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def inline_schema_refs(schema: dict[str, Any] | bool) -> Any:
|
|
40
|
+
if not isinstance(schema, dict):
|
|
41
|
+
return schema
|
|
42
|
+
defs: dict[str, Any] | None = schema.get("$defs")
|
|
43
|
+
if not defs:
|
|
44
|
+
return schema
|
|
45
|
+
schema = copy.copy(schema)
|
|
46
|
+
schema.pop("$defs")
|
|
47
|
+
return _inline_schema_refs(schema, {f"#/$defs/{k}": v for k, v in defs.items()})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _inline_schema_refs(schema: dict[str, Any] | bool, defs: dict[str, Any]) -> Any:
|
|
51
|
+
if not isinstance(schema, dict):
|
|
52
|
+
return schema
|
|
53
|
+
if "$ref" in schema:
|
|
54
|
+
ref = schema["$ref"]
|
|
55
|
+
if ref in defs:
|
|
56
|
+
ref_schema = _inline_schema_refs(defs[ref], defs)
|
|
57
|
+
schema = copy.copy(schema)
|
|
58
|
+
schema.pop("$ref")
|
|
59
|
+
schema.update(copy.deepcopy(ref_schema))
|
|
60
|
+
return schema
|
|
61
|
+
schema = copy.copy(schema)
|
|
62
|
+
for k in ("allOf", "anyOf", "oneOf"):
|
|
63
|
+
if k in schema:
|
|
64
|
+
if isinstance(schema[k], list):
|
|
65
|
+
schema[k] = [_inline_schema_refs(s, defs) for s in schema[k]]
|
|
66
|
+
for k in ("items", "prefixItems", "additionalProperties"):
|
|
67
|
+
if k in schema:
|
|
68
|
+
if isinstance(schema[k], list):
|
|
69
|
+
schema[k] = [_inline_schema_refs(s, defs) for s in schema[k]]
|
|
70
|
+
else:
|
|
71
|
+
schema[k] = _inline_schema_refs(schema[k], defs)
|
|
72
|
+
for k in ("properties",):
|
|
73
|
+
if k in schema:
|
|
74
|
+
if isinstance(schema[k], dict):
|
|
75
|
+
schema[k] = {
|
|
76
|
+
k: _inline_schema_refs(s, defs) for k, s in schema[k].items()
|
|
77
|
+
}
|
|
78
|
+
return schema
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def json_schema_to_openapi_schema(
|
|
82
|
+
schema: dict[str, Any], *, openapi_spec: str = "3.0"
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
"""
|
|
85
|
+
Convert a JSON schema `dict` object into an OpenAPI schema `dict` object.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
schema: A JSON schema `dict` object.
|
|
89
|
+
openapi_spec: The OpenAPI specification.
|
|
90
|
+
Currently, only `"3.0"` is supported.
|
|
91
|
+
Returns:
|
|
92
|
+
An OpenAPI schema `dict` object.
|
|
93
|
+
"""
|
|
94
|
+
ensure_condition(openapi_spec == "3.0", "openapi_spec must be '3.0'")
|
|
95
|
+
return _json_schema_to_openapi_schema_3_0(copy.deepcopy(schema))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _json_schema_to_openapi_schema_3_0(schema: dict[str, Any]) -> dict[str, Any]:
|
|
99
|
+
if "type" in schema:
|
|
100
|
+
type_ = schema["type"]
|
|
101
|
+
if type_ == "null":
|
|
102
|
+
schema.pop("type")
|
|
103
|
+
schema["nullable"] = True
|
|
104
|
+
if "prefixItems" in schema:
|
|
105
|
+
# This is how pydantic encodes tuples.
|
|
106
|
+
items = schema.pop("prefixItems")
|
|
107
|
+
if isinstance(items, (list, tuple)):
|
|
108
|
+
schema["items"] = _json_tuple_to_openapi_items_3_0(items)
|
|
109
|
+
if "items" in schema:
|
|
110
|
+
items = schema.pop("items")
|
|
111
|
+
if isinstance(items, (list, tuple)):
|
|
112
|
+
schema["items"] = _json_tuple_to_openapi_items_3_0(items)
|
|
113
|
+
elif isinstance(items, dict):
|
|
114
|
+
schema["items"] = _json_schema_to_openapi_schema_3_0(items)
|
|
115
|
+
if "additionalProperties" in schema:
|
|
116
|
+
additional_properties = schema.pop("additionalProperties")
|
|
117
|
+
if isinstance(additional_properties, dict):
|
|
118
|
+
schema["additionalProperties"] = _json_schema_to_openapi_schema_3_0(
|
|
119
|
+
additional_properties
|
|
120
|
+
)
|
|
121
|
+
elif additional_properties in (True, None):
|
|
122
|
+
schema["additionalProperties"] = {}
|
|
123
|
+
elif additional_properties is False:
|
|
124
|
+
schema["additionalProperties"] = False
|
|
125
|
+
for dict_key in ("properties", "$defs"):
|
|
126
|
+
if dict_key in schema and isinstance(schema[dict_key], dict):
|
|
127
|
+
schema[dict_key] = {
|
|
128
|
+
k: _json_schema_to_openapi_schema_3_0(v)
|
|
129
|
+
for k, v in schema[dict_key].items()
|
|
130
|
+
}
|
|
131
|
+
for x_of_key in ("allOf", "anyOf", "oneOf"):
|
|
132
|
+
if x_of_key in schema:
|
|
133
|
+
schema[x_of_key] = list(
|
|
134
|
+
map(_json_schema_to_openapi_schema_3_0, schema[x_of_key])
|
|
135
|
+
)
|
|
136
|
+
if len(schema[x_of_key]) == 2:
|
|
137
|
+
x_of_copy = [s for s in schema[x_of_key] if s != {"nullable": True}]
|
|
138
|
+
if len(x_of_copy) == 1:
|
|
139
|
+
schema.pop(x_of_key)
|
|
140
|
+
schema.update(x_of_copy[0])
|
|
141
|
+
schema["nullable"] = True
|
|
142
|
+
return schema
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _json_tuple_to_openapi_items_3_0(
|
|
146
|
+
tuple_schemas: Sequence[dict[str, Any]],
|
|
147
|
+
) -> dict[str, Any]:
|
|
148
|
+
"""Convert a JSON schema tuple."""
|
|
149
|
+
if not tuple_schemas:
|
|
150
|
+
return {}
|
|
151
|
+
converted = [_json_schema_to_openapi_schema_3_0(v) for v in tuple_schemas]
|
|
152
|
+
c0 = converted[0]
|
|
153
|
+
if all(c == c0 for c in converted[1:]):
|
|
154
|
+
# If all items are the same schema,
|
|
155
|
+
# we can take the first.
|
|
156
|
+
return c0
|
|
157
|
+
# As there is no way to represent tuples in OpenAPI <= 3.1,
|
|
158
|
+
# we set items to oneOf of all tuple member schemas.
|
|
159
|
+
return {"oneOf": converted}
|
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "procodile"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.1.1"
|
|
8
8
|
description = "A light-weight processor development framework"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
11
|
-
{name = "
|
|
12
|
-
{name = "
|
|
11
|
+
{name = "b-yogesh", email = "b-yogesh@users.noreply.github.com"},
|
|
12
|
+
{name = "forman", email = "forman@users.noreply.github.com"},
|
|
13
|
+
{name = "pont-us", email = "pont-us@users.noreply.github.com"},
|
|
14
|
+
{name = "TejasMorbagal", email = "TejasMorbagal@users.noreply.github.com"},
|
|
13
15
|
]
|
|
14
16
|
keywords = [
|
|
15
17
|
"eo",
|
|
@@ -24,10 +26,10 @@ classifiers = [
|
|
|
24
26
|
"Intended Audience :: Developers",
|
|
25
27
|
"License :: OSI Approved :: Apache Software License",
|
|
26
28
|
"Programming Language :: Python :: 3",
|
|
27
|
-
"Programming Language :: Python :: 3.10",
|
|
28
29
|
"Programming Language :: Python :: 3.11",
|
|
29
30
|
"Programming Language :: Python :: 3.12",
|
|
30
31
|
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Programming Language :: Python :: 3.14",
|
|
31
33
|
"Topic :: Software Development",
|
|
32
34
|
"Topic :: Scientific/Engineering",
|
|
33
35
|
"Typing :: Typed",
|
|
@@ -42,7 +44,7 @@ dependencies = [
|
|
|
42
44
|
"pyyaml",
|
|
43
45
|
"typer",
|
|
44
46
|
# local dependency
|
|
45
|
-
"gavicore>=0.
|
|
47
|
+
"gavicore >=0.1.1",
|
|
46
48
|
]
|
|
47
49
|
|
|
48
50
|
[project.urls]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|