api-foundry-query-engine 0.8.68__tar.gz → 0.8.70__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.
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/PKG-INFO +1 -1
- api_foundry_query_engine-0.8.70/api_foundry_query_engine/__init__.py +1 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/connectors/postgres_connection.py +9 -4
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/dao/sql_insert_query_handler.py +1 -1
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/dao/sql_query_handler.py +69 -2
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/dao/sql_select_query_handler.py +1 -1
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/utils/token_decoder.py +121 -2
- api_foundry_query_engine-0.8.68/api_foundry_query_engine/__init__.py +0 -1
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/.gitignore +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/LICENSE +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/adapters/adapter.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/adapters/gateway_adapter.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/connectors/connection.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/dao/batch_operation_handler.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/dao/dao.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/dao/operation_dao.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/dao/sql_custom_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/dao/sql_delete_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/dao/sql_restore_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/dao/sql_update_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/lambda_handler.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/operation.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/services/service.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/services/transactional_service.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/utils/api_model.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/utils/app_exception.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/utils/claims_check.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/utils/dependency_resolver.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/utils/logger.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/api_foundry_query_engine/utils/reference_resolver.py +0 -0
- {api_foundry_query_engine-0.8.68 → api_foundry_query_engine-0.8.70}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api-foundry-query-engine
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.70
|
|
4
4
|
Summary: The AWS lambda service handler use by the `api_foundry` project is a powerful tool designed to automate the deployment of REST APIs on AWS using Lambda services to access and interact with relational databases (RDBMS). This project leverages the OpenAPI specification to define and manage the APIs
|
|
5
5
|
Project-URL: Documentation, https://github.com/DanRepik/api-foundry
|
|
6
6
|
Project-URL: Source, https://github.com/DanRepik/api-foundry
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.70"
|
|
@@ -32,12 +32,17 @@ class PostgresCursor(Cursor):
|
|
|
32
32
|
try:
|
|
33
33
|
# Execute the SQL statement with parameters
|
|
34
34
|
self.__cursor.execute(sql, params)
|
|
35
|
+
selected_columns = set(selection_results)
|
|
36
|
+
column_names = [desc[0] for desc in (self.__cursor.description or ())]
|
|
35
37
|
result = []
|
|
36
38
|
for record in self.__cursor:
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
# Use cursor metadata so SQL aliases define the output mapping.
|
|
40
|
+
row = {
|
|
41
|
+
col: value
|
|
42
|
+
for col, value in zip(column_names, record)
|
|
43
|
+
if col in selected_columns
|
|
44
|
+
}
|
|
45
|
+
result.append(row)
|
|
41
46
|
|
|
42
47
|
return result
|
|
43
48
|
except IntegrityError as err:
|
|
@@ -59,7 +59,7 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
59
59
|
)
|
|
60
60
|
# Combine both (union of read and write)
|
|
61
61
|
combined = {**read_props, **write_props}
|
|
62
|
-
filters = self.operation.metadata_params.get("_properties", ".*")
|
|
62
|
+
filters = self.tokenize_selector_filters(self.operation.metadata_params.get("_properties", ".*"))
|
|
63
63
|
self._insert_selection_results = self.filter_and_prefix_keys(filters, combined)
|
|
64
64
|
return self._insert_selection_results
|
|
65
65
|
|
|
@@ -97,12 +97,15 @@ RELATIONAL_TYPES = {
|
|
|
97
97
|
"lt": "<",
|
|
98
98
|
"le": "<=",
|
|
99
99
|
"eq": "=",
|
|
100
|
+
"ne": "<>",
|
|
100
101
|
"ge": ">=",
|
|
101
102
|
"gt": ">",
|
|
102
103
|
"in": "in",
|
|
103
104
|
"not-in": "not-in",
|
|
104
105
|
"between": "between",
|
|
105
106
|
"not-between": "not-between",
|
|
107
|
+
"like": "like",
|
|
108
|
+
"not-like": "not-like",
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
|
|
@@ -355,6 +358,8 @@ class SQLQueryHandler:
|
|
|
355
358
|
self.placeholder(property, f"{placeholder_name}_{index}") for index, _ in enumerate(value_set)
|
|
356
359
|
]
|
|
357
360
|
sql = f"{column} {'NOT ' if operand == 'not-in' else ''}IN ({', '.join(assignments)})" # noqa E501
|
|
361
|
+
elif operand in ["like", "not-like"]:
|
|
362
|
+
sql = f"{column} {'NOT ' if operand == 'not-like' else ''}LIKE {self.placeholder(property, str(placeholder_name))}"
|
|
358
363
|
else:
|
|
359
364
|
sql = f"{column} {operand} {self.placeholder(property, str(placeholder_name))}"
|
|
360
365
|
return sql
|
|
@@ -385,11 +390,16 @@ class SQLQueryHandler:
|
|
|
385
390
|
for index, item in enumerate(value_set):
|
|
386
391
|
item_name = f"{placeholder_name}_{index}"
|
|
387
392
|
placeholders[item_name] = property.convert_to_db_value(item)
|
|
393
|
+
elif operand in ["like", "not-like"]:
|
|
394
|
+
placeholders = {placeholder_name: self._normalize_like_pattern(value_str)}
|
|
388
395
|
else:
|
|
389
396
|
placeholders = {placeholder_name: property.convert_to_db_value(value_str)}
|
|
390
397
|
|
|
391
398
|
return placeholders
|
|
392
399
|
|
|
400
|
+
def _normalize_like_pattern(self, value: str) -> str:
|
|
401
|
+
return value.replace("*", "%").replace("?", "_")
|
|
402
|
+
|
|
393
403
|
def search_value_assignment(
|
|
394
404
|
self, property: SchemaObjectProperty, value, prefix: Optional[str] = None
|
|
395
405
|
) -> tuple[str, dict]:
|
|
@@ -508,13 +518,70 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
|
|
|
508
518
|
log.info("selection_result")
|
|
509
519
|
if not hasattr(self, "__selection_results"):
|
|
510
520
|
log.info("prefix_map: %s", self.prefix_map)
|
|
511
|
-
filters = self.operation.metadata_params.get("_properties", ".*")
|
|
521
|
+
filters = self.tokenize_selector_filters(self.operation.metadata_params.get("_properties", ".*"))
|
|
512
522
|
allowed_properties = self.check_permissions(
|
|
513
523
|
"read", self.schema_object.permissions, self.schema_object.properties
|
|
514
524
|
)
|
|
515
525
|
self.__selection_results = self.filter_and_prefix_keys(filters, allowed_properties)
|
|
516
526
|
return self.__selection_results
|
|
517
527
|
|
|
528
|
+
@staticmethod
|
|
529
|
+
def tokenize_selector_filters(filter_str: str) -> List[str]:
|
|
530
|
+
"""Split selector strings on commas or whitespace outside regex groups.
|
|
531
|
+
|
|
532
|
+
This preserves commas used inside regex syntax such as `{1,3}` or `[a,b]`
|
|
533
|
+
while allowing callers to pass selectors as either space-delimited,
|
|
534
|
+
comma-delimited, or mixed.
|
|
535
|
+
"""
|
|
536
|
+
if not filter_str:
|
|
537
|
+
return []
|
|
538
|
+
|
|
539
|
+
filters: List[str] = []
|
|
540
|
+
current: List[str] = []
|
|
541
|
+
bracket_depth = 0
|
|
542
|
+
brace_depth = 0
|
|
543
|
+
paren_depth = 0
|
|
544
|
+
escaped = False
|
|
545
|
+
|
|
546
|
+
for char in filter_str:
|
|
547
|
+
if escaped:
|
|
548
|
+
current.append(char)
|
|
549
|
+
escaped = False
|
|
550
|
+
continue
|
|
551
|
+
|
|
552
|
+
if char == "\\":
|
|
553
|
+
current.append(char)
|
|
554
|
+
escaped = True
|
|
555
|
+
continue
|
|
556
|
+
|
|
557
|
+
if char == "[":
|
|
558
|
+
bracket_depth += 1
|
|
559
|
+
elif char == "]" and bracket_depth > 0:
|
|
560
|
+
bracket_depth -= 1
|
|
561
|
+
elif char == "{":
|
|
562
|
+
brace_depth += 1
|
|
563
|
+
elif char == "}" and brace_depth > 0:
|
|
564
|
+
brace_depth -= 1
|
|
565
|
+
elif char == "(":
|
|
566
|
+
paren_depth += 1
|
|
567
|
+
elif char == ")" and paren_depth > 0:
|
|
568
|
+
paren_depth -= 1
|
|
569
|
+
|
|
570
|
+
if char in {",", " ", "\t", "\n", "\r"} and not (bracket_depth or brace_depth or paren_depth):
|
|
571
|
+
token = "".join(current).strip()
|
|
572
|
+
if token:
|
|
573
|
+
filters.append(token)
|
|
574
|
+
current = []
|
|
575
|
+
continue
|
|
576
|
+
|
|
577
|
+
current.append(char)
|
|
578
|
+
|
|
579
|
+
token = "".join(current).strip()
|
|
580
|
+
if token:
|
|
581
|
+
filters.append(token)
|
|
582
|
+
|
|
583
|
+
return filters
|
|
584
|
+
|
|
518
585
|
def _has_soft_delete_conflicts(self) -> Dict[str, bool]:
|
|
519
586
|
"""
|
|
520
587
|
Detect conflicts between query parameters and soft delete exclusions.
|
|
@@ -626,7 +693,7 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
|
|
|
626
693
|
if (
|
|
627
694
|
self.operation.action != "read"
|
|
628
695
|
and isinstance(value, str)
|
|
629
|
-
and re.match(r"^(lt|le|eq|ne|gt|ge|in|not-in|between|not-between)::(.+)$", value)
|
|
696
|
+
and re.match(r"^(lt|le|eq|ne|gt|ge|in|not-in|between|not-between|like|not-like)::(.+)$", value)
|
|
630
697
|
and self.schema_object.concurrency_property
|
|
631
698
|
):
|
|
632
699
|
raise ApplicationException(
|
|
@@ -303,7 +303,7 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
303
303
|
def get_regex_map(self, filter_str: str) -> dict:
|
|
304
304
|
result = {}
|
|
305
305
|
|
|
306
|
-
for flt in
|
|
306
|
+
for flt in self.tokenize_selector_filters(filter_str):
|
|
307
307
|
parts = flt.split(":")
|
|
308
308
|
entity = parts[0] if len(parts) > 1 else self.schema_object.api_name
|
|
309
309
|
expression = parts[-1]
|
|
@@ -20,6 +20,13 @@ __all__ = ["token_decoder"]
|
|
|
20
20
|
|
|
21
21
|
log = logging.getLogger(__name__)
|
|
22
22
|
|
|
23
|
+
METHOD_TO_ACTION = {
|
|
24
|
+
"GET": "read",
|
|
25
|
+
"POST": "create",
|
|
26
|
+
"PUT": "update",
|
|
27
|
+
"DELETE": "delete",
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
|
|
24
31
|
def _log_jwt_configuration():
|
|
25
32
|
"""Log current JWT configuration for debugging."""
|
|
@@ -114,6 +121,87 @@ class LambdaTokenValidator:
|
|
|
114
121
|
raise ValueError(f"Token validation failed: {str(e)}")
|
|
115
122
|
|
|
116
123
|
|
|
124
|
+
def _normalize_permission_action(action: str) -> str:
|
|
125
|
+
return "write" if action in ("create", "update") else action
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _extract_event_entity_and_action(event: Dict[str, Any]) -> tuple[Optional[str], str]:
|
|
129
|
+
resource = event.get("resource") or event.get("path") or ""
|
|
130
|
+
parts = [p for p in str(resource).split("/") if p]
|
|
131
|
+
api_prefixes = {"api", "v1", "v2", "v3"}
|
|
132
|
+
literal_parts = [
|
|
133
|
+
p
|
|
134
|
+
for p in parts
|
|
135
|
+
if p not in api_prefixes and not (p.startswith("{") and p.endswith("}"))
|
|
136
|
+
]
|
|
137
|
+
entity = "_".join(literal_parts) if literal_parts else None
|
|
138
|
+
method = str(event.get("httpMethod", "")).upper()
|
|
139
|
+
action = METHOD_TO_ACTION.get(method, "read")
|
|
140
|
+
return entity, action
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _normalize_permissions(permissions: Optional[dict]) -> dict:
|
|
144
|
+
if not permissions or not isinstance(permissions, dict):
|
|
145
|
+
return {}
|
|
146
|
+
|
|
147
|
+
if "default" in permissions:
|
|
148
|
+
return permissions
|
|
149
|
+
|
|
150
|
+
normalized = {"default": {"read": {}, "write": {}, "delete": {}}}
|
|
151
|
+
for role, actions in permissions.items():
|
|
152
|
+
if not isinstance(actions, dict):
|
|
153
|
+
continue
|
|
154
|
+
for action, rule in actions.items():
|
|
155
|
+
normalized_action = _normalize_permission_action(action)
|
|
156
|
+
if normalized_action in normalized["default"]:
|
|
157
|
+
normalized["default"][normalized_action][role] = rule
|
|
158
|
+
return normalized
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _get_route_permissions(event: Dict[str, Any]) -> dict:
|
|
162
|
+
from api_foundry_query_engine.utils import api_model as api_model_module
|
|
163
|
+
from api_foundry_query_engine.utils.api_model import (
|
|
164
|
+
get_path_operation,
|
|
165
|
+
get_schema_object,
|
|
166
|
+
set_api_model,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if api_model_module.api_model is None:
|
|
170
|
+
if not os.getenv("API_SPEC"):
|
|
171
|
+
return {}
|
|
172
|
+
try:
|
|
173
|
+
set_api_model(os.environ)
|
|
174
|
+
except Exception as exc:
|
|
175
|
+
log.warning("Failed to load API model for anonymous-route check: %s", exc)
|
|
176
|
+
return {}
|
|
177
|
+
|
|
178
|
+
entity, action = _extract_event_entity_and_action(event)
|
|
179
|
+
if not entity:
|
|
180
|
+
return {}
|
|
181
|
+
|
|
182
|
+
path_operation = get_path_operation(entity, action)
|
|
183
|
+
if path_operation:
|
|
184
|
+
return path_operation.permissions or {}
|
|
185
|
+
|
|
186
|
+
schema_object = get_schema_object(entity)
|
|
187
|
+
if schema_object:
|
|
188
|
+
return schema_object.permissions or {}
|
|
189
|
+
|
|
190
|
+
return {}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _route_explicitly_allows_role(event: Dict[str, Any], role: Optional[str]) -> bool:
|
|
194
|
+
if not role:
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
_, action = _extract_event_entity_and_action(event)
|
|
198
|
+
permissions = _normalize_permissions(_get_route_permissions(event))
|
|
199
|
+
action_permissions = permissions.get("default", {}).get(
|
|
200
|
+
_normalize_permission_action(action), {}
|
|
201
|
+
)
|
|
202
|
+
return role in action_permissions
|
|
203
|
+
|
|
204
|
+
|
|
117
205
|
def token_decoder(
|
|
118
206
|
jwks_url: Optional[str] = None,
|
|
119
207
|
audience: Optional[str] = None,
|
|
@@ -223,16 +311,47 @@ def token_decoder(
|
|
|
223
311
|
log.debug("config_anonymous_role: %s", config_anonymous_role)
|
|
224
312
|
log.debug("config_lambda_arn: %s", config_lambda_arn)
|
|
225
313
|
|
|
314
|
+
# Some API Gateway routes are intentionally public but still receive
|
|
315
|
+
# browser Authorization headers from the SPA. In Lambda proxy mode
|
|
316
|
+
# those public routes do not have requestContext.authorizer populated,
|
|
317
|
+
# and invoking a Lambda TOKEN validator from a VPC-backed API Lambda
|
|
318
|
+
# can hang until the API timeout. When anonymous access is explicitly
|
|
319
|
+
# enabled and claims checks are disabled, treat the request as public
|
|
320
|
+
# unless API Gateway already supplied verified claims above.
|
|
321
|
+
if (
|
|
322
|
+
config_anonymous_role
|
|
323
|
+
and os.getenv("SKIP_CLAIMS_CHECK", "").lower() == "true"
|
|
324
|
+
and _route_explicitly_allows_role(event, config_anonymous_role)
|
|
325
|
+
):
|
|
326
|
+
log.debug(
|
|
327
|
+
"SKIP_CLAIMS_CHECK enabled; using anonymous role without inline token validation"
|
|
328
|
+
)
|
|
329
|
+
if "requestContext" not in event:
|
|
330
|
+
event["requestContext"] = {}
|
|
331
|
+
event["requestContext"]["authorizer"] = {
|
|
332
|
+
"roles": [config_anonymous_role]
|
|
333
|
+
}
|
|
334
|
+
return handler(event, context)
|
|
335
|
+
|
|
226
336
|
# Skip all validation if no method configured
|
|
227
337
|
if not config_lambda_arn and not config_jwks_url:
|
|
228
338
|
log.debug("No validation method configured")
|
|
229
|
-
if config_anonymous_role:
|
|
339
|
+
if _route_explicitly_allows_role(event, config_anonymous_role):
|
|
230
340
|
log.debug("Using anonymous role: %s", config_anonymous_role)
|
|
231
341
|
if "requestContext" not in event:
|
|
232
342
|
event["requestContext"] = {}
|
|
233
343
|
event["requestContext"]["authorizer"] = {
|
|
234
344
|
"roles": [config_anonymous_role]
|
|
235
345
|
}
|
|
346
|
+
elif config_anonymous_role:
|
|
347
|
+
log.error(
|
|
348
|
+
"Anonymous role configured but route does not explicitly allow it"
|
|
349
|
+
)
|
|
350
|
+
return {
|
|
351
|
+
"statusCode": 401,
|
|
352
|
+
"headers": {"Content-Type": "application/json"},
|
|
353
|
+
"body": json.dumps({"error": "Unauthorized"}),
|
|
354
|
+
}
|
|
236
355
|
return handler(event, context)
|
|
237
356
|
|
|
238
357
|
try:
|
|
@@ -315,7 +434,7 @@ def token_decoder(
|
|
|
315
434
|
|
|
316
435
|
# Fall back to anonymous if configured
|
|
317
436
|
if not decoded_token:
|
|
318
|
-
if wrapper._anonymous_role:
|
|
437
|
+
if _route_explicitly_allows_role(event, wrapper._anonymous_role):
|
|
319
438
|
log.debug(
|
|
320
439
|
"Using anonymous role: %s",
|
|
321
440
|
wrapper._anonymous_role,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.8.68"
|
|
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
|