cuenca-validations 2.0.0.dev4__tar.gz → 2.0.0.dev5__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.
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/PKG-INFO +1 -1
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/errors.py +0 -13
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/card.py +12 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/general.py +13 -28
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/identities.py +10 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/validators.py +5 -0
- cuenca_validations-2.0.0.dev5/cuenca_validations/version.py +1 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations.egg-info/PKG-INFO +1 -1
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations.egg-info/SOURCES.txt +1 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/tests/test_card.py +5 -6
- cuenca_validations-2.0.0.dev5/tests/test_errors.py +59 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/tests/test_types.py +79 -9
- cuenca_validations-2.0.0.dev4/cuenca_validations/version.py +0 -1
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/LICENSE +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/README.md +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/__init__.py +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/card_bins.py +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/py.typed +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/__init__.py +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/enums.py +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/files.py +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/morals.py +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/queries.py +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/requests.py +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/typing.py +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations.egg-info/dependency_links.txt +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations.egg-info/requires.txt +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations.egg-info/top_level.txt +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/setup.cfg +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/setup.py +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/tests/__init__.py +0 -0
- {cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/tests/test_statement.py +0 -0
{cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/errors.py
RENAMED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
__all__ = [
|
|
2
2
|
'ApiError',
|
|
3
3
|
'AuthMethodNotAllowedError',
|
|
4
|
-
'CardBinValidationError',
|
|
5
4
|
'CuencaError',
|
|
6
5
|
'ERROR_CODES',
|
|
7
6
|
'InvalidOTPCodeError',
|
|
@@ -13,18 +12,6 @@ __all__ = [
|
|
|
13
12
|
'WrongCredsError',
|
|
14
13
|
]
|
|
15
14
|
|
|
16
|
-
from pydantic_core import PydanticCustomError
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class CardBinValidationError(PydanticCustomError):
|
|
20
|
-
code = 'payment_card_number.bin'
|
|
21
|
-
msg_template = (
|
|
22
|
-
'The card number contains a BIN (first six digits) that does not have'
|
|
23
|
-
'a known association with a Mexican bank. To add the association,'
|
|
24
|
-
'please file an issue:'
|
|
25
|
-
'https://github.com/cuenca-mx/cuenca-validations/issues'
|
|
26
|
-
)
|
|
27
|
-
|
|
28
15
|
|
|
29
16
|
class CuencaError(Exception):
|
|
30
17
|
"""Exceptions related to ApiKeys, Login, Password, etc"""
|
{cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/card.py
RENAMED
|
@@ -21,6 +21,18 @@ class StrictPaymentCardNumber(BaseModel):
|
|
|
21
21
|
def brand(self) -> PaymentCardBrand:
|
|
22
22
|
return self.card_number.brand
|
|
23
23
|
|
|
24
|
+
@property
|
|
25
|
+
def last4(self) -> str:
|
|
26
|
+
return self.card_number.last4
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def masked(self) -> str:
|
|
30
|
+
return self.card_number.masked
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def bin(self) -> str:
|
|
34
|
+
return self.card_number.bin
|
|
35
|
+
|
|
24
36
|
@property
|
|
25
37
|
def bank_code(self) -> str:
|
|
26
38
|
return CARD_BINS[self.card_number.bin]
|
{cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/general.py
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Any, Optional
|
|
3
3
|
|
|
4
4
|
from pydantic import BeforeValidator, Field
|
|
5
5
|
from typing_extensions import Annotated
|
|
@@ -47,34 +47,19 @@ StrictPositiveFloat = Annotated[
|
|
|
47
47
|
]
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@classmethod
|
|
60
|
-
def validate_digits(cls, value: str) -> str:
|
|
61
|
-
if not value.isdigit():
|
|
62
|
-
raise ValueError("Value must contain only digits.")
|
|
63
|
-
if cls.min_length is not None and len(value) < cls.min_length:
|
|
64
|
-
raise ValueError(
|
|
65
|
-
f"Value must have at least {cls.min_length} characters."
|
|
66
|
-
)
|
|
67
|
-
if cls.max_length is not None and len(value) > cls.max_length:
|
|
68
|
-
raise ValueError(
|
|
69
|
-
f"Value must have at most {cls.max_length} characters."
|
|
70
|
-
)
|
|
71
|
-
return value
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
# Función para crear tipos personalizados
|
|
75
|
-
def digits(min_length: Optional[int] = None, max_length: Optional[int] = None):
|
|
50
|
+
def validate_only_digits(value: str) -> str:
|
|
51
|
+
if not value.isdigit():
|
|
52
|
+
raise ValueError("Value must contain only digits")
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def digits(
|
|
57
|
+
min_length: Optional[int] = None, max_length: Optional[int] = None
|
|
58
|
+
) -> Any:
|
|
76
59
|
return Annotated[
|
|
77
|
-
str,
|
|
60
|
+
str,
|
|
61
|
+
BeforeValidator(validate_only_digits),
|
|
62
|
+
Field(min_length=min_length, max_length=max_length),
|
|
78
63
|
]
|
|
79
64
|
|
|
80
65
|
|
|
@@ -58,6 +58,16 @@ class Rfc(StrictStr):
|
|
|
58
58
|
raise ValueError('Not a valid RFC.')
|
|
59
59
|
return cls(rfc)
|
|
60
60
|
|
|
61
|
+
@classmethod
|
|
62
|
+
def __get_pydantic_core_schema__(
|
|
63
|
+
cls, source_type: Any, handler: Any
|
|
64
|
+
) -> Dict[str, Any]:
|
|
65
|
+
return {
|
|
66
|
+
'type': 'str',
|
|
67
|
+
'min_length': cls.min_length,
|
|
68
|
+
'max_length': cls.max_length,
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
|
|
62
72
|
class Address(BaseModel):
|
|
63
73
|
street: Optional[str] = None
|
{cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/validators.py
RENAMED
|
@@ -4,6 +4,7 @@ from enum import Enum
|
|
|
4
4
|
from typing import Any, Callable, List, Optional, Union
|
|
5
5
|
|
|
6
6
|
from dateutil.relativedelta import relativedelta
|
|
7
|
+
from pydantic import AnyUrl, HttpUrl
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
def sanitize_dict(d: dict) -> dict:
|
|
@@ -26,6 +27,10 @@ def sanitize_item(
|
|
|
26
27
|
rv = item.astimezone(dt.timezone.utc).isoformat()
|
|
27
28
|
else:
|
|
28
29
|
rv = item.isoformat()
|
|
30
|
+
elif isinstance(item, HttpUrl):
|
|
31
|
+
rv = str(item)
|
|
32
|
+
elif isinstance(item, AnyUrl):
|
|
33
|
+
rv = str(item)
|
|
29
34
|
elif isinstance(item, list):
|
|
30
35
|
rv = [
|
|
31
36
|
sanitize_dict(e) if isinstance(e, dict) else sanitize_item(e)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '2.0.0.dev5'
|
|
@@ -10,15 +10,14 @@ INVALID_BIN = '4050000000000001'
|
|
|
10
10
|
|
|
11
11
|
def test_invalid_bin_strict_payment():
|
|
12
12
|
with pytest.raises(ValidationError) as exc_info:
|
|
13
|
-
|
|
14
|
-
print(card.card_number.bin)
|
|
13
|
+
StrictPaymentCardNumber(card_number=INVALID_BIN)
|
|
15
14
|
assert 'Invalid BIN: Bank code not found.' in str(exc_info.value)
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
def test_valid_bin_strict_payment():
|
|
19
18
|
card = StrictPaymentCardNumber(card_number=VALID_BBVA)
|
|
20
|
-
assert card.
|
|
21
|
-
assert card.
|
|
22
|
-
assert card.
|
|
23
|
-
assert card.
|
|
19
|
+
assert card.brand == PaymentCardBrand.visa
|
|
20
|
+
assert card.bin == '477213'
|
|
21
|
+
assert card.last4 == '0003'
|
|
22
|
+
assert card.masked == '477213******0003'
|
|
24
23
|
assert card.bank_code == '40012'
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from cuenca_validations.errors import (
|
|
4
|
+
ERROR_CODES,
|
|
5
|
+
ApiError,
|
|
6
|
+
AuthMethodNotAllowedError,
|
|
7
|
+
CuencaError,
|
|
8
|
+
InvalidOTPCodeError,
|
|
9
|
+
MissingAuthorizationHeaderError,
|
|
10
|
+
NoPasswordFoundError,
|
|
11
|
+
TooManyAttemptsError,
|
|
12
|
+
UserLocationError,
|
|
13
|
+
UserNotLoggedInError,
|
|
14
|
+
WrongCredsError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_cuenca_error_base():
|
|
19
|
+
assert issubclass(CuencaError, Exception)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_error_codes_and_status():
|
|
23
|
+
test_cases = [
|
|
24
|
+
(WrongCredsError, 101, 401),
|
|
25
|
+
(MissingAuthorizationHeaderError, 102, 401),
|
|
26
|
+
(UserNotLoggedInError, 103, 401),
|
|
27
|
+
(NoPasswordFoundError, 104, 401),
|
|
28
|
+
(AuthMethodNotAllowedError, 106, 401),
|
|
29
|
+
(TooManyAttemptsError, 107, 403),
|
|
30
|
+
(UserLocationError, 108, 401),
|
|
31
|
+
(InvalidOTPCodeError, 109, 401),
|
|
32
|
+
(ApiError, 500, 500),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
for error_class, expected_code, expected_status in test_cases:
|
|
36
|
+
assert error_class.code == expected_code
|
|
37
|
+
assert error_class.status_code == expected_status
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_error_messages():
|
|
41
|
+
test_message = "Mensaje de prueba"
|
|
42
|
+
|
|
43
|
+
for error_class in ERROR_CODES.values():
|
|
44
|
+
with pytest.raises(error_class) as exc_info:
|
|
45
|
+
raise error_class(test_message)
|
|
46
|
+
assert str(exc_info.value) == test_message
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_error_codes_mapping():
|
|
50
|
+
for error_class in ERROR_CODES.values():
|
|
51
|
+
assert ERROR_CODES[error_class.code] == error_class
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_api_error():
|
|
55
|
+
with pytest.raises(ApiError) as exc_info:
|
|
56
|
+
raise ApiError("Error interno")
|
|
57
|
+
assert str(exc_info.value) == "Error interno"
|
|
58
|
+
assert exc_info.value.code == 500
|
|
59
|
+
assert exc_info.value.status_code == 500
|
|
@@ -5,7 +5,7 @@ from enum import Enum
|
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
from freezegun import freeze_time
|
|
8
|
-
from pydantic import BaseModel,
|
|
8
|
+
from pydantic import AnyUrl, BaseModel, HttpUrl, ValidationError
|
|
9
9
|
|
|
10
10
|
from cuenca_validations.types import (
|
|
11
11
|
Address,
|
|
@@ -16,6 +16,7 @@ from cuenca_validations.types import (
|
|
|
16
16
|
SantizedDict,
|
|
17
17
|
SessionRequest,
|
|
18
18
|
TransactionStatus,
|
|
19
|
+
digits,
|
|
19
20
|
get_state_name,
|
|
20
21
|
)
|
|
21
22
|
from cuenca_validations.types.enums import (
|
|
@@ -24,6 +25,10 @@ from cuenca_validations.types.enums import (
|
|
|
24
25
|
SessionType,
|
|
25
26
|
State,
|
|
26
27
|
)
|
|
28
|
+
from cuenca_validations.types.general import (
|
|
29
|
+
StrictPositiveFloat,
|
|
30
|
+
StrictPositiveInt,
|
|
31
|
+
)
|
|
27
32
|
from cuenca_validations.types.requests import (
|
|
28
33
|
ApiKeyUpdateRequest,
|
|
29
34
|
BankAccountValidationRequest,
|
|
@@ -127,6 +132,17 @@ def test_json_encoder(value, result):
|
|
|
127
132
|
assert decoded['value'] == result
|
|
128
133
|
|
|
129
134
|
|
|
135
|
+
def test_sanitized_dict_with_urls():
|
|
136
|
+
data = SantizedDict(
|
|
137
|
+
api_url=HttpUrl('https://api.cuenca.com/v1'),
|
|
138
|
+
ftp_url=AnyUrl('ftp://files.example.com/'),
|
|
139
|
+
)
|
|
140
|
+
assert data == {
|
|
141
|
+
'api_url': 'https://api.cuenca.com/v1',
|
|
142
|
+
'ftp_url': 'ftp://files.example.com/',
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
130
146
|
def test_invalid_class():
|
|
131
147
|
"""
|
|
132
148
|
For a class that doesn't have a `to_dict` method and it is not a type of
|
|
@@ -143,9 +159,7 @@ def test_invalid_class():
|
|
|
143
159
|
|
|
144
160
|
|
|
145
161
|
class Accounts(BaseModel):
|
|
146
|
-
number:
|
|
147
|
-
min_length=5, max_length=8, pattern=r'^\d+$' # Only allows digits
|
|
148
|
-
)
|
|
162
|
+
number: digits(5, 8) # type: ignore
|
|
149
163
|
|
|
150
164
|
|
|
151
165
|
def test_only_digits():
|
|
@@ -156,15 +170,15 @@ def test_only_digits():
|
|
|
156
170
|
@pytest.mark.parametrize(
|
|
157
171
|
'number, error',
|
|
158
172
|
[
|
|
159
|
-
('123', '
|
|
160
|
-
('1234567890', '
|
|
161
|
-
('no_123', '
|
|
173
|
+
('123', 'Value should have at least 5 items after validation'),
|
|
174
|
+
('1234567890', 'Value should have at most 8 items after validation'),
|
|
175
|
+
('no_123', 'Value must contain only digits'),
|
|
162
176
|
],
|
|
163
177
|
)
|
|
164
178
|
def test_invalid_digits(number, error):
|
|
165
|
-
with pytest.raises(
|
|
179
|
+
with pytest.raises(ValidationError) as exception:
|
|
166
180
|
Accounts(number=number)
|
|
167
|
-
assert error in str(exception)
|
|
181
|
+
assert error in str(exception.value)
|
|
168
182
|
|
|
169
183
|
|
|
170
184
|
def test_card_query_exp_cvv_if_number_set():
|
|
@@ -566,3 +580,59 @@ def test_user_lists_request():
|
|
|
566
580
|
UserListsRequest(names='Pedro', first_surname='Paramo')
|
|
567
581
|
with pytest.raises(ValueError):
|
|
568
582
|
UserListsRequest()
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class TestFloatModel(BaseModel):
|
|
586
|
+
value: StrictPositiveFloat
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def test_strict_positive_float_valid():
|
|
590
|
+
model = TestFloatModel(value=10.5)
|
|
591
|
+
assert model.value == 10.5
|
|
592
|
+
model = TestFloatModel(value=0.000001)
|
|
593
|
+
assert model.value == 0.000001
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def test_strict_positive_float_invalid():
|
|
597
|
+
with pytest.raises(ValueError, match="Value must be greater than 0"):
|
|
598
|
+
TestFloatModel(value=0.0)
|
|
599
|
+
with pytest.raises(ValueError, match="Value must be greater than 0"):
|
|
600
|
+
TestFloatModel(value=-1.5)
|
|
601
|
+
with pytest.raises(ValueError, match="Value must be a float"):
|
|
602
|
+
TestFloatModel(value=5)
|
|
603
|
+
with pytest.raises(ValueError, match="Value must be a float"):
|
|
604
|
+
TestFloatModel(value="10.5")
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
class TestIntModel(BaseModel):
|
|
608
|
+
value: StrictPositiveInt
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def test_strict_positive_int_valid():
|
|
612
|
+
model = TestIntModel(value=100)
|
|
613
|
+
assert model.value == 100
|
|
614
|
+
|
|
615
|
+
model = TestIntModel(value=1)
|
|
616
|
+
assert model.value == 1
|
|
617
|
+
|
|
618
|
+
model = TestIntModel(value=21_474_836_47)
|
|
619
|
+
assert model.value == 21_474_836_47
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def test_strict_positive_int_invalid():
|
|
623
|
+
with pytest.raises(ValueError, match="Value must be greater than 0"):
|
|
624
|
+
TestIntModel(value=0)
|
|
625
|
+
|
|
626
|
+
with pytest.raises(ValueError, match="Value must be greater than 0"):
|
|
627
|
+
TestIntModel(value=-5)
|
|
628
|
+
|
|
629
|
+
with pytest.raises(
|
|
630
|
+
ValueError, match="Value must be less than 21_474_836_47"
|
|
631
|
+
):
|
|
632
|
+
TestIntModel(value=21_474_836_48)
|
|
633
|
+
|
|
634
|
+
with pytest.raises(ValueError, match="Value must be an integer"):
|
|
635
|
+
TestIntModel(value=5.5)
|
|
636
|
+
|
|
637
|
+
with pytest.raises(ValueError, match="Value must be an integer"):
|
|
638
|
+
TestIntModel(value="10")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '2.0.0.dev4'
|
|
File without changes
|
|
File without changes
|
{cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/__init__.py
RENAMED
|
File without changes
|
{cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/card_bins.py
RENAMED
|
File without changes
|
|
File without changes
|
{cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/__init__.py
RENAMED
|
File without changes
|
{cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/enums.py
RENAMED
|
File without changes
|
{cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/files.py
RENAMED
|
File without changes
|
{cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/morals.py
RENAMED
|
File without changes
|
{cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/queries.py
RENAMED
|
File without changes
|
{cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/types/requests.py
RENAMED
|
File without changes
|
{cuenca_validations-2.0.0.dev4 → cuenca_validations-2.0.0.dev5}/cuenca_validations/typing.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|