dana-python 2.1.0__py3-none-any.whl → 2.1.2__py3-none-any.whl
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.
- dana/disbursement/v1/api/disbursement_api.py +18 -4
- dana/exceptions.py +5 -1
- dana/payment_gateway/v1/custom_validation.py +195 -124
- dana/utils/script.py +16 -1
- dana/widget/v1/custom_validation.py +66 -34
- {dana_python-2.1.0.dist-info → dana_python-2.1.2.dist-info}/METADATA +4 -4
- {dana_python-2.1.0.dist-info → dana_python-2.1.2.dist-info}/RECORD +10 -10
- {dana_python-2.1.0.dist-info → dana_python-2.1.2.dist-info}/WHEEL +1 -1
- {dana_python-2.1.0.dist-info → dana_python-2.1.2.dist-info}/licenses/LICENSE +0 -0
- {dana_python-2.1.0.dist-info → dana_python-2.1.2.dist-info}/top_level.txt +0 -0
|
@@ -1238,6 +1238,13 @@ class DisbursementApi:
|
|
|
1238
1238
|
)
|
|
1239
1239
|
|
|
1240
1240
|
|
|
1241
|
+
|
|
1242
|
+
def _get_transfer_to_dana_path(self) -> str:
|
|
1243
|
+
"""Transfer to DANA (topup) path based on environment."""
|
|
1244
|
+
env = os.getenv("DANA_ENV", os.getenv("ENV", "sandbox")).lower()
|
|
1245
|
+
return '/v1.0/emoney/topup.htm' if env == 'production' else '/rest/v1.0/emoney/topup'
|
|
1246
|
+
|
|
1247
|
+
|
|
1241
1248
|
@validate_call
|
|
1242
1249
|
def transfer_to_dana(
|
|
1243
1250
|
self,
|
|
@@ -1503,7 +1510,7 @@ class DisbursementApi:
|
|
|
1503
1510
|
_auth_settings = SnapHeader.merge_with_snap_runtime_headers(_auth_settings)
|
|
1504
1511
|
_generated_auth = SnapHeader.get_snap_generated_auth(
|
|
1505
1512
|
method='POST',
|
|
1506
|
-
resource_path=
|
|
1513
|
+
resource_path=(self._get_transfer_to_dana_path()),
|
|
1507
1514
|
body=transfer_to_dana_request.to_json(),
|
|
1508
1515
|
private_key=self.api_client.configuration.get_api_key_with_prefix('PRIVATE_KEY'),
|
|
1509
1516
|
private_key_path=self.api_client.configuration.get_api_key_with_prefix('PRIVATE_KEY_PATH')
|
|
@@ -1511,7 +1518,7 @@ class DisbursementApi:
|
|
|
1511
1518
|
|
|
1512
1519
|
return self.api_client.param_serialize(
|
|
1513
1520
|
method='POST',
|
|
1514
|
-
resource_path=
|
|
1521
|
+
resource_path=(self._get_transfer_to_dana_path()),
|
|
1515
1522
|
path_params=_path_params,
|
|
1516
1523
|
query_params=_query_params,
|
|
1517
1524
|
header_params=_header_params,
|
|
@@ -1526,6 +1533,13 @@ class DisbursementApi:
|
|
|
1526
1533
|
)
|
|
1527
1534
|
|
|
1528
1535
|
|
|
1536
|
+
|
|
1537
|
+
def _get_transfer_to_dana_inquiry_status_path(self) -> str:
|
|
1538
|
+
"""Topup-status inquiry path based on environment."""
|
|
1539
|
+
env = os.getenv("DANA_ENV", os.getenv("ENV", "sandbox")).lower()
|
|
1540
|
+
return '/v1.0/emoney/topup-status.htm' if env == 'production' else '/rest/v1.0/emoney/topup-status'
|
|
1541
|
+
|
|
1542
|
+
|
|
1529
1543
|
@validate_call
|
|
1530
1544
|
def transfer_to_dana_inquiry_status(
|
|
1531
1545
|
self,
|
|
@@ -1791,7 +1805,7 @@ class DisbursementApi:
|
|
|
1791
1805
|
_auth_settings = SnapHeader.merge_with_snap_runtime_headers(_auth_settings)
|
|
1792
1806
|
_generated_auth = SnapHeader.get_snap_generated_auth(
|
|
1793
1807
|
method='POST',
|
|
1794
|
-
resource_path=
|
|
1808
|
+
resource_path=(self._get_transfer_to_dana_inquiry_status_path()),
|
|
1795
1809
|
body=transfer_to_dana_inquiry_status_request.to_json(),
|
|
1796
1810
|
private_key=self.api_client.configuration.get_api_key_with_prefix('PRIVATE_KEY'),
|
|
1797
1811
|
private_key_path=self.api_client.configuration.get_api_key_with_prefix('PRIVATE_KEY_PATH')
|
|
@@ -1799,7 +1813,7 @@ class DisbursementApi:
|
|
|
1799
1813
|
|
|
1800
1814
|
return self.api_client.param_serialize(
|
|
1801
1815
|
method='POST',
|
|
1802
|
-
resource_path=
|
|
1816
|
+
resource_path=(self._get_transfer_to_dana_inquiry_status_path()),
|
|
1803
1817
|
path_params=_path_params,
|
|
1804
1818
|
query_params=_query_params,
|
|
1805
1819
|
header_params=_header_params,
|
dana/exceptions.py
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
Do not edit the class manually.
|
|
23
23
|
""" # noqa: E501
|
|
24
24
|
|
|
25
|
-
from typing import Any, Optional
|
|
25
|
+
from typing import Any, Dict, List, Optional
|
|
26
26
|
from typing_extensions import Self
|
|
27
27
|
|
|
28
28
|
class OpenApiException(Exception):
|
|
@@ -122,7 +122,11 @@ class ApiException(OpenApiException):
|
|
|
122
122
|
*,
|
|
123
123
|
body: Optional[str] = None,
|
|
124
124
|
data: Optional[Any] = None,
|
|
125
|
+
contexts: Optional[List[Dict[str, str]]] = None,
|
|
125
126
|
) -> None:
|
|
127
|
+
self.contexts = contexts
|
|
128
|
+
if contexts is not None and reason is None:
|
|
129
|
+
reason = "; ".join(f"{c['field']}: {c['message']}" for c in contexts)
|
|
126
130
|
self.status = status
|
|
127
131
|
self.reason = reason
|
|
128
132
|
self.body = body
|
|
@@ -21,33 +21,38 @@ Validations are registered in the validation_registry and executed via custom_va
|
|
|
21
21
|
|
|
22
22
|
import os
|
|
23
23
|
import re
|
|
24
|
-
from typing import Any, Dict, List,
|
|
24
|
+
from typing import Any, Callable, Dict, FrozenSet, List, Set
|
|
25
|
+
|
|
25
26
|
from dana.utils.date_validation import validate_valid_up_to_date
|
|
26
27
|
from dana.exceptions import ApiException
|
|
27
28
|
|
|
28
29
|
# Money value pattern: digits (1-16) + "." + exactly 2 digits (e.g. 10000.00)
|
|
29
30
|
MONEY_VALUE_PATTERN = re.compile(r'^\d{1,16}\.\d{2}$')
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
SANDBOX_ALLOWED_PAY_METHODS = frozenset({
|
|
32
|
+
SANDBOX_ALLOWED_PAY_METHODS: FrozenSet[str] = frozenset({
|
|
33
33
|
'BALANCE', 'CREDIT_CARD', 'DEBIT_CARD', 'VIRTUAL_ACCOUNT', 'NETWORK_PAY',
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
# API may send full identifiers (e.g. VIRTUAL_ACCOUNT_BRI); we accept exact match or suffix _OPTION.
|
|
38
|
-
SANDBOX_ALLOWED_PAY_OPTIONS = frozenset({
|
|
36
|
+
SANDBOX_ALLOWED_PAY_OPTIONS: FrozenSet[str] = frozenset({
|
|
39
37
|
'CARD', 'QRIS', 'BRI', 'PANIN', 'CIMB', 'MANDIRI', 'BTPN',
|
|
40
38
|
})
|
|
41
39
|
|
|
40
|
+
CREDIT_DEBIT_CARD_PAY_METHODS: Set[str] = {'CREDIT_CARD', 'DEBIT_CARD'}
|
|
41
|
+
NETWORK_PAY_PG_CARD = 'NETWORK_PAY_PG_CARD'
|
|
42
|
+
EWALLET_PAY_OPTIONS: Set[str] = {
|
|
43
|
+
'NETWORK_PAY_PG_SPAY',
|
|
44
|
+
'NETWORK_PAY_PG_OVO',
|
|
45
|
+
'NETWORK_PAY_PG_GOPAY',
|
|
46
|
+
'NETWORK_PAY_PG_LINKAJA',
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
|
|
43
50
|
def _is_sandbox() -> bool:
|
|
44
|
-
"""Return True if current environment is sandbox (DANA_ENV or ENV)."""
|
|
45
51
|
env = os.getenv('DANA_ENV', os.getenv('ENV', 'sandbox')).lower()
|
|
46
52
|
return env == 'sandbox'
|
|
47
53
|
|
|
48
54
|
|
|
49
55
|
def _pay_option_allowed_in_sandbox(value: str) -> bool:
|
|
50
|
-
"""Check if payOption value is allowed in sandbox (exact or suffix match, e.g. VIRTUAL_ACCOUNT_BRI)."""
|
|
51
56
|
if not value or not str(value).strip():
|
|
52
57
|
return False
|
|
53
58
|
s = str(value).strip()
|
|
@@ -59,160 +64,222 @@ def _pay_option_allowed_in_sandbox(value: str) -> bool:
|
|
|
59
64
|
return False
|
|
60
65
|
|
|
61
66
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
def _normalize_value(value: Any) -> str:
|
|
68
|
+
if value is None:
|
|
69
|
+
return ''
|
|
70
|
+
if hasattr(value, 'value'):
|
|
71
|
+
return str(getattr(value, 'value')).strip()
|
|
72
|
+
return str(value).strip()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _trim_str(value: Any) -> str:
|
|
76
|
+
if value is None:
|
|
77
|
+
return ''
|
|
78
|
+
return str(value).strip()
|
|
79
|
+
|
|
65
80
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
def _rune_len(s: str) -> int:
|
|
82
|
+
return len(list(s))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _ctx(field: str, message: str) -> Dict[str, str]:
|
|
86
|
+
return {'field': field, 'message': message}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def validate_additional_info_required(request: Any) -> None:
|
|
69
90
|
if request is None:
|
|
70
91
|
return
|
|
71
92
|
if hasattr(request, 'additional_info') and request.additional_info is None:
|
|
72
|
-
raise ApiException(
|
|
73
|
-
status=0,
|
|
74
|
-
reason='additionalInfo is required'
|
|
75
|
-
)
|
|
93
|
+
raise ApiException(status=0, contexts=[_ctx('additionalInfo', 'additionalInfo is required')])
|
|
76
94
|
|
|
77
95
|
|
|
78
96
|
def validate_money_value_pattern(request: Any) -> None:
|
|
79
|
-
"""
|
|
80
|
-
Validate that Money value matches pattern (e.g. 10000.00): ^\\d{1,16}\\.\\d{2}$
|
|
81
|
-
|
|
82
|
-
Raises:
|
|
83
|
-
ApiException: If amount.value is missing or does not match pattern
|
|
84
|
-
"""
|
|
85
97
|
if request is None:
|
|
86
98
|
return
|
|
87
99
|
if not hasattr(request, 'amount') or request.amount is None:
|
|
88
100
|
return
|
|
89
101
|
value = getattr(request.amount, 'value', None)
|
|
90
102
|
if value is None or value == '':
|
|
91
|
-
raise ApiException(
|
|
92
|
-
status=0,
|
|
93
|
-
reason='amount.value is required'
|
|
94
|
-
)
|
|
103
|
+
raise ApiException(status=0, contexts=[_ctx('amount.value', 'amount.value is required')])
|
|
95
104
|
if not MONEY_VALUE_PATTERN.match(str(value)):
|
|
96
|
-
raise ApiException(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
105
|
+
raise ApiException(status=0, contexts=[
|
|
106
|
+
_ctx('amount.value', f'amount.value must match pattern (e.g. 10000.00): got {value!r}')
|
|
107
|
+
])
|
|
100
108
|
|
|
101
109
|
|
|
102
110
|
def validate_valid_up_to_create_order_request(request: Any) -> None:
|
|
103
|
-
"""
|
|
104
|
-
Validate validUpTo field in CreateOrderByApiRequest or CreateOrderByRedirectRequest.
|
|
105
|
-
|
|
106
|
-
This function handles both request types directly (not wrapped in CreateOrderRequest):
|
|
107
|
-
- CreateOrderByApiRequest: validates valid_up_to directly
|
|
108
|
-
- CreateOrderByRedirectRequest: validates valid_up_to directly
|
|
109
|
-
|
|
110
|
-
Args:
|
|
111
|
-
request: The request object to validate
|
|
112
|
-
|
|
113
|
-
Raises:
|
|
114
|
-
ApiException: If validation fails
|
|
115
|
-
"""
|
|
116
111
|
if request is None:
|
|
117
112
|
return
|
|
118
|
-
|
|
119
|
-
# Handle CreateOrderByApiRequest or CreateOrderByRedirectRequest directly
|
|
120
113
|
if hasattr(request, 'valid_up_to') and request.valid_up_to is not None:
|
|
121
114
|
try:
|
|
122
115
|
validate_valid_up_to_date(request.valid_up_to)
|
|
123
116
|
except ValueError as e:
|
|
124
|
-
raise ApiException(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
) from e
|
|
117
|
+
raise ApiException(status=0, contexts=[
|
|
118
|
+
_ctx('validUpTo', f'validUpTo validation failed: {str(e)}')
|
|
119
|
+
]) from e
|
|
128
120
|
|
|
129
121
|
|
|
130
122
|
def validate_external_store_id_for_qris(request: Any) -> None:
|
|
131
|
-
"""
|
|
132
|
-
Validate that externalStoreId is required when payOption is NETWORK_PAY_PG_QRIS.
|
|
133
|
-
|
|
134
|
-
This function checks if any payOption in payOptionDetails is NETWORK_PAY_PG_QRIS,
|
|
135
|
-
and if so, ensures externalStoreId is provided.
|
|
136
|
-
|
|
137
|
-
Args:
|
|
138
|
-
request: The request object to validate
|
|
139
|
-
|
|
140
|
-
Raises:
|
|
141
|
-
ApiException: If validation fails
|
|
142
|
-
"""
|
|
143
123
|
if request is None:
|
|
144
124
|
return
|
|
145
|
-
|
|
146
|
-
# Check if request has payOptionDetails
|
|
147
125
|
if not hasattr(request, 'pay_option_details') or request.pay_option_details is None:
|
|
148
126
|
return
|
|
149
|
-
|
|
150
|
-
# Check if any payOption is NETWORK_PAY_PG_QRIS
|
|
151
127
|
has_qris = False
|
|
152
128
|
if isinstance(request.pay_option_details, list):
|
|
153
129
|
for pay_option_detail in request.pay_option_details:
|
|
154
130
|
if hasattr(pay_option_detail, 'pay_option') and pay_option_detail.pay_option == 'NETWORK_PAY_PG_QRIS':
|
|
155
131
|
has_qris = True
|
|
156
132
|
break
|
|
157
|
-
|
|
158
|
-
# If QRIS is found, externalStoreId must be provided
|
|
159
133
|
if has_qris:
|
|
160
134
|
external_store_id = None
|
|
161
135
|
if hasattr(request, 'external_store_id'):
|
|
162
136
|
external_store_id = request.external_store_id
|
|
163
|
-
|
|
164
137
|
if not external_store_id or (isinstance(external_store_id, str) and external_store_id.strip() == ''):
|
|
165
|
-
raise ApiException(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
)
|
|
138
|
+
raise ApiException(status=0, contexts=[
|
|
139
|
+
_ctx('externalStoreId', 'externalStoreId is required when payOption is NETWORK_PAY_PG_QRIS')
|
|
140
|
+
])
|
|
169
141
|
|
|
170
142
|
|
|
171
143
|
def validate_sandbox_pay_method_and_pay_option(request: Any) -> None:
|
|
172
|
-
"""
|
|
173
|
-
In sandbox, only certain payMethod and payOption values are available.
|
|
174
|
-
|
|
175
|
-
- payMethod: only BALANCE, CREDIT_CARD, DEBIT_CARD, VIRTUAL_ACCOUNT, NETWORK_PAY.
|
|
176
|
-
- payOption: only CARD, QRIS, BRI, PANIN, CIMB, MANDIRI, BTPN (exact or suffix, e.g. VIRTUAL_ACCOUNT_BRI).
|
|
177
|
-
|
|
178
|
-
Skipped when not in sandbox (DANA_ENV / ENV != sandbox).
|
|
179
|
-
|
|
180
|
-
Raises:
|
|
181
|
-
ApiException: If in sandbox and a payMethod or payOption is not allowed.
|
|
182
|
-
"""
|
|
183
144
|
if request is None or not _is_sandbox():
|
|
184
145
|
return
|
|
185
|
-
|
|
146
|
+
pay_option_details = getattr(request, 'pay_option_details', None)
|
|
147
|
+
if not pay_option_details or not isinstance(pay_option_details, list):
|
|
186
148
|
return
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
149
|
+
for i, detail in enumerate(pay_option_details):
|
|
150
|
+
if not detail:
|
|
151
|
+
continue
|
|
190
152
|
if hasattr(detail, 'pay_method') and detail.pay_method is not None:
|
|
191
|
-
|
|
192
|
-
pm_str = str(pm).strip()
|
|
153
|
+
pm_str = _normalize_value(detail.pay_method)
|
|
193
154
|
if pm_str and pm_str not in SANDBOX_ALLOWED_PAY_METHODS:
|
|
194
|
-
raise ApiException(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
155
|
+
raise ApiException(status=0, contexts=[
|
|
156
|
+
_ctx(
|
|
157
|
+
f'payOptionDetails[{i}].payMethod',
|
|
158
|
+
(
|
|
159
|
+
f'In sandbox, payMethod must be one of [{", ".join(sorted(SANDBOX_ALLOWED_PAY_METHODS))}]; '
|
|
160
|
+
f'got {pm_str}'
|
|
161
|
+
),
|
|
199
162
|
)
|
|
200
|
-
)
|
|
163
|
+
])
|
|
201
164
|
if hasattr(detail, 'pay_option') and detail.pay_option is not None:
|
|
202
|
-
|
|
203
|
-
po_str = str(po).strip()
|
|
204
|
-
# Empty payOption is allowed (e.g. for BALANCE)
|
|
165
|
+
po_str = _normalize_value(detail.pay_option)
|
|
205
166
|
if po_str and not _pay_option_allowed_in_sandbox(po_str):
|
|
206
|
-
raise ApiException(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
167
|
+
raise ApiException(status=0, contexts=[
|
|
168
|
+
_ctx(
|
|
169
|
+
f'payOptionDetails[{i}].payOption',
|
|
170
|
+
(
|
|
171
|
+
f'In sandbox, payOption must be one of [{", ".join(sorted(SANDBOX_ALLOWED_PAY_OPTIONS))}] '
|
|
172
|
+
f'(or suffix like VIRTUAL_ACCOUNT_BRI); got {po_str}'
|
|
173
|
+
),
|
|
211
174
|
)
|
|
175
|
+
])
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def validate_conditional_pay_option_additional_info_create_order_request(request: Any) -> None:
|
|
179
|
+
if request is None:
|
|
180
|
+
return
|
|
181
|
+
pay_option_details = getattr(request, 'pay_option_details', None)
|
|
182
|
+
if not pay_option_details or not isinstance(pay_option_details, list):
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
contexts: List[Dict[str, str]] = []
|
|
186
|
+
|
|
187
|
+
for i, detail in enumerate(pay_option_details):
|
|
188
|
+
if not detail:
|
|
189
|
+
continue
|
|
190
|
+
pay_method = _trim_str(getattr(detail, 'pay_method', None))
|
|
191
|
+
pay_option = _trim_str(getattr(detail, 'pay_option', None))
|
|
192
|
+
additional_info = getattr(detail, 'additional_info', None)
|
|
193
|
+
phone_raw = None
|
|
194
|
+
if additional_info is not None:
|
|
195
|
+
phone_raw = getattr(additional_info, 'phone_number', None)
|
|
196
|
+
phone_number = _trim_str(phone_raw)
|
|
197
|
+
|
|
198
|
+
is_card = pay_method in CREDIT_DEBIT_CARD_PAY_METHODS or pay_option == NETWORK_PAY_PG_CARD
|
|
199
|
+
is_ewallet = pay_option in EWALLET_PAY_OPTIONS
|
|
200
|
+
|
|
201
|
+
if is_card or is_ewallet:
|
|
202
|
+
field = f'payOptionDetails[{i}].additionalInfo.phoneNumber'
|
|
203
|
+
if not phone_number:
|
|
204
|
+
contexts.append(
|
|
205
|
+
_ctx(field, f'phoneNumber is required for card/e-wallet payment (payOptionDetails[{i}])')
|
|
212
206
|
)
|
|
207
|
+
else:
|
|
208
|
+
ln = _rune_len(phone_number)
|
|
209
|
+
if ln < 1 or ln > 15:
|
|
210
|
+
contexts.append(
|
|
211
|
+
_ctx(field, f'phoneNumber must be between 1 and 15 characters (payOptionDetails[{i}])')
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if contexts:
|
|
215
|
+
raise ApiException(status=0, contexts=contexts)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def validate_optional_fields_with_required_nested_create_order_request(request: Any) -> None:
|
|
219
|
+
if request is None:
|
|
220
|
+
return
|
|
221
|
+
additional_info = getattr(request, 'additional_info', None)
|
|
222
|
+
if additional_info is None:
|
|
223
|
+
return
|
|
224
|
+
order = getattr(additional_info, 'order', None)
|
|
225
|
+
if not order:
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
contexts: List[Dict[str, str]] = []
|
|
229
|
+
|
|
230
|
+
buyer = getattr(order, 'buyer', None)
|
|
231
|
+
if buyer:
|
|
232
|
+
ext_type = _trim_str(getattr(buyer, 'external_user_type', None))
|
|
233
|
+
ext_id = _trim_str(getattr(buyer, 'external_user_id', None))
|
|
234
|
+
has_type = bool(ext_type)
|
|
235
|
+
has_id = bool(ext_id)
|
|
236
|
+
if has_id and not has_type:
|
|
237
|
+
contexts.append(
|
|
238
|
+
_ctx(
|
|
239
|
+
'additionalInfo.order.buyer.externalUserType',
|
|
240
|
+
'externalUserType is required when externalUserId is filled',
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
if has_type and not has_id:
|
|
244
|
+
contexts.append(
|
|
245
|
+
_ctx(
|
|
246
|
+
'additionalInfo.order.buyer.externalUserId',
|
|
247
|
+
'externalUserId is required when externalUserType is filled',
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
goods = getattr(order, 'goods', None)
|
|
252
|
+
if isinstance(goods, list) and len(goods) > 0:
|
|
253
|
+
for i, g in enumerate(goods):
|
|
254
|
+
if not g:
|
|
255
|
+
continue
|
|
256
|
+
name = _trim_str(getattr(g, 'name', None))
|
|
257
|
+
if not name:
|
|
258
|
+
contexts.append(
|
|
259
|
+
_ctx(
|
|
260
|
+
f'additionalInfo.order.goods[{i}].name',
|
|
261
|
+
'name is required when goods is filled',
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
shipping_info = getattr(order, 'shipping_info', None)
|
|
266
|
+
if isinstance(shipping_info, list) and len(shipping_info) > 0:
|
|
267
|
+
for i, s in enumerate(shipping_info):
|
|
268
|
+
if not s:
|
|
269
|
+
continue
|
|
270
|
+
first_name = _trim_str(getattr(s, 'first_name', None))
|
|
271
|
+
if not first_name:
|
|
272
|
+
contexts.append(
|
|
273
|
+
_ctx(
|
|
274
|
+
f'additionalInfo.order.shippingInfo[{i}].firstName',
|
|
275
|
+
'firstName is required when shippingInfo is filled',
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
if contexts:
|
|
280
|
+
raise ApiException(status=0, contexts=contexts)
|
|
213
281
|
|
|
214
282
|
|
|
215
|
-
# Validation registry maps request class names to their validation functions
|
|
216
283
|
validation_registry: Dict[str, List[Callable[[Any], None]]] = {
|
|
217
284
|
'CreateOrderByApiRequest': [
|
|
218
285
|
validate_additional_info_required,
|
|
@@ -220,12 +287,16 @@ validation_registry: Dict[str, List[Callable[[Any], None]]] = {
|
|
|
220
287
|
validate_valid_up_to_create_order_request,
|
|
221
288
|
validate_external_store_id_for_qris,
|
|
222
289
|
validate_sandbox_pay_method_and_pay_option,
|
|
290
|
+
validate_conditional_pay_option_additional_info_create_order_request,
|
|
291
|
+
validate_optional_fields_with_required_nested_create_order_request,
|
|
223
292
|
],
|
|
224
293
|
'CreateOrderByRedirectRequest': [
|
|
225
294
|
validate_additional_info_required,
|
|
226
295
|
validate_money_value_pattern,
|
|
227
296
|
validate_valid_up_to_create_order_request,
|
|
228
297
|
validate_sandbox_pay_method_and_pay_option,
|
|
298
|
+
validate_conditional_pay_option_additional_info_create_order_request,
|
|
299
|
+
validate_optional_fields_with_required_nested_create_order_request,
|
|
229
300
|
],
|
|
230
301
|
'CreateOrderRequest': [
|
|
231
302
|
validate_additional_info_required,
|
|
@@ -233,30 +304,30 @@ validation_registry: Dict[str, List[Callable[[Any], None]]] = {
|
|
|
233
304
|
validate_valid_up_to_create_order_request,
|
|
234
305
|
validate_external_store_id_for_qris,
|
|
235
306
|
validate_sandbox_pay_method_and_pay_option,
|
|
307
|
+
validate_conditional_pay_option_additional_info_create_order_request,
|
|
308
|
+
validate_optional_fields_with_required_nested_create_order_request,
|
|
236
309
|
],
|
|
237
|
-
# Add more request types and their validations here as needed
|
|
238
310
|
}
|
|
239
311
|
|
|
240
312
|
|
|
241
313
|
def custom_validation(request: Any) -> None:
|
|
242
|
-
"""
|
|
243
|
-
Perform custom validations on the request based on its type.
|
|
244
|
-
|
|
245
|
-
This function checks the request type and runs the appropriate validations from the registry.
|
|
246
|
-
|
|
247
|
-
Args:
|
|
248
|
-
request: The request object to validate (can be any type)
|
|
249
|
-
|
|
250
|
-
Raises:
|
|
251
|
-
ApiException: If validation fails
|
|
252
|
-
"""
|
|
314
|
+
"""Run all validators for the request type and aggregate client validation contexts."""
|
|
253
315
|
if request is None:
|
|
254
316
|
return
|
|
255
|
-
|
|
256
|
-
# Get the class name of the request
|
|
317
|
+
|
|
257
318
|
class_name = request.__class__.__name__
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
319
|
+
if class_name not in validation_registry:
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
aggregated: List[Dict[str, str]] = []
|
|
323
|
+
for validator in validation_registry[class_name]:
|
|
324
|
+
try:
|
|
262
325
|
validator(request)
|
|
326
|
+
except ApiException as e:
|
|
327
|
+
if e.contexts:
|
|
328
|
+
aggregated.extend(e.contexts)
|
|
329
|
+
else:
|
|
330
|
+
raise
|
|
331
|
+
|
|
332
|
+
if aggregated:
|
|
333
|
+
raise ApiException(status=0, contexts=aggregated)
|
dana/utils/script.py
CHANGED
|
@@ -15,10 +15,23 @@
|
|
|
15
15
|
import importlib
|
|
16
16
|
import pkgutil
|
|
17
17
|
import os
|
|
18
|
+
import re
|
|
18
19
|
from typing import List
|
|
19
20
|
|
|
20
21
|
PACKAGE_NAME = 'dana'
|
|
21
22
|
|
|
23
|
+
# Only single path segments that are valid Python identifiers (no traversal / injection).
|
|
24
|
+
_SAFE_PKG_SEGMENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _assert_safe_import_segment(name: str, label: str) -> None:
|
|
28
|
+
if not name or not _SAFE_PKG_SEGMENT.fullmatch(name):
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"Refusing dynamic import: disallowed {label} {name!r} "
|
|
31
|
+
"(expected a single identifier-like package segment)"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
22
35
|
def import_all_models(base_path: str) -> None:
|
|
23
36
|
"""
|
|
24
37
|
Imports all model modules from subdirectories of the specified base path.
|
|
@@ -48,6 +61,8 @@ def import_all_models(base_path: str) -> None:
|
|
|
48
61
|
path_to_domain: List[str] = getattr(subdomain.module_finder, 'path', '')
|
|
49
62
|
domain = os.path.basename(path_to_domain)
|
|
50
63
|
|
|
51
|
-
# Construct the full module path and import
|
|
64
|
+
# Construct the full module path and import (whitelist segments — no dynamic/untrusted strings)
|
|
65
|
+
_assert_safe_import_segment(domain, "domain")
|
|
66
|
+
_assert_safe_import_segment(subdomain.name, "subdomain")
|
|
52
67
|
module_name = f"{PACKAGE_NAME}.{domain}.{subdomain.name}.models"
|
|
53
68
|
importlib.import_module(module_name)
|
|
@@ -19,62 +19,94 @@ This module provides custom validation functions for Widget API requests.
|
|
|
19
19
|
Validations are registered in the validation_registry and executed via custom_validation().
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
from typing import Any, Dict, List
|
|
22
|
+
from typing import Any, Callable, Dict, List
|
|
23
|
+
|
|
23
24
|
from dana.utils.date_validation import validate_valid_up_to_date
|
|
24
25
|
from dana.exceptions import ApiException
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
def _ctx(field: str, message: str) -> Dict[str, str]:
|
|
29
|
+
return {'field': field, 'message': message}
|
|
30
|
+
|
|
31
|
+
|
|
27
32
|
def validate_valid_up_to_widget_payment_request(request: Any) -> None:
|
|
28
|
-
"""
|
|
29
|
-
Validate validUpTo field in WidgetPaymentRequest.
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
request: The request object to validate
|
|
33
|
-
|
|
34
|
-
Raises:
|
|
35
|
-
ApiException: If validation fails
|
|
36
|
-
"""
|
|
33
|
+
"""Validate validUpTo field in WidgetPaymentRequest."""
|
|
37
34
|
if request is None:
|
|
38
35
|
return
|
|
39
|
-
|
|
40
36
|
if hasattr(request, 'valid_up_to') and request.valid_up_to is not None:
|
|
41
37
|
try:
|
|
42
38
|
validate_valid_up_to_date(request.valid_up_to)
|
|
43
39
|
except ValueError as e:
|
|
44
|
-
raise ApiException(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
raise ApiException(status=0, contexts=[
|
|
41
|
+
_ctx('validUpTo', f'validUpTo validation failed: {str(e)}')
|
|
42
|
+
]) from e
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _contains_forbidden_auth_code_delimiters(auth_code: str) -> bool:
|
|
46
|
+
return '&' in auth_code or '=' in auth_code
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_apply_token_auth_code_authorization_code(request: Any) -> None:
|
|
50
|
+
"""authCode must not contain URL query delimiter characters (pasted query string)."""
|
|
51
|
+
if request is None:
|
|
52
|
+
return
|
|
53
|
+
auth_code = getattr(request, 'auth_code', None)
|
|
54
|
+
if auth_code is None:
|
|
55
|
+
return
|
|
56
|
+
s = str(auth_code)
|
|
57
|
+
if s and _contains_forbidden_auth_code_delimiters(s):
|
|
58
|
+
raise ApiException(status=0, contexts=[
|
|
59
|
+
_ctx('authCode', "authCode must not contain URL query delimiter characters ('&' or '=')")
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def validate_apply_token_auth_code_refresh_token(request: Any) -> None:
|
|
64
|
+
"""If authCode is present on refresh-token request, apply the same rule."""
|
|
65
|
+
if request is None:
|
|
66
|
+
return
|
|
67
|
+
auth_code = getattr(request, 'auth_code', None)
|
|
68
|
+
if auth_code is None:
|
|
69
|
+
return
|
|
70
|
+
trimmed = str(auth_code).strip()
|
|
71
|
+
if not trimmed:
|
|
72
|
+
return
|
|
73
|
+
if _contains_forbidden_auth_code_delimiters(trimmed):
|
|
74
|
+
raise ApiException(status=0, contexts=[
|
|
75
|
+
_ctx('authCode', "authCode must not contain URL query delimiter characters ('&' or '=')")
|
|
76
|
+
])
|
|
48
77
|
|
|
49
78
|
|
|
50
|
-
# Validation registry maps request class names to their validation functions
|
|
51
79
|
validation_registry: Dict[str, List[Callable[[Any], None]]] = {
|
|
52
80
|
'WidgetPaymentRequest': [
|
|
53
81
|
validate_valid_up_to_widget_payment_request,
|
|
54
82
|
],
|
|
55
|
-
|
|
83
|
+
'ApplyTokenAuthorizationCodeRequest': [
|
|
84
|
+
validate_apply_token_auth_code_authorization_code,
|
|
85
|
+
],
|
|
86
|
+
'ApplyTokenRefreshTokenRequest': [
|
|
87
|
+
validate_apply_token_auth_code_refresh_token,
|
|
88
|
+
],
|
|
56
89
|
}
|
|
57
90
|
|
|
58
91
|
|
|
59
92
|
def custom_validation(request: Any) -> None:
|
|
60
|
-
"""
|
|
61
|
-
Perform custom validations on the request based on its type.
|
|
62
|
-
|
|
63
|
-
This function checks the request type and runs the appropriate validations from the registry.
|
|
64
|
-
|
|
65
|
-
Args:
|
|
66
|
-
request: The request object to validate (can be any type)
|
|
67
|
-
|
|
68
|
-
Raises:
|
|
69
|
-
ApiException: If validation fails
|
|
70
|
-
"""
|
|
93
|
+
"""Run all validators for the request type and aggregate client validation contexts."""
|
|
71
94
|
if request is None:
|
|
72
95
|
return
|
|
73
|
-
|
|
74
|
-
# Get the class name of the request
|
|
96
|
+
|
|
75
97
|
class_name = request.__class__.__name__
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
98
|
+
if class_name not in validation_registry:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
aggregated: List[Dict[str, str]] = []
|
|
102
|
+
for validator in validation_registry[class_name]:
|
|
103
|
+
try:
|
|
80
104
|
validator(request)
|
|
105
|
+
except ApiException as e:
|
|
106
|
+
if e.contexts:
|
|
107
|
+
aggregated.extend(e.contexts)
|
|
108
|
+
else:
|
|
109
|
+
raise
|
|
110
|
+
|
|
111
|
+
if aggregated:
|
|
112
|
+
raise ApiException(status=0, contexts=aggregated)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dana-python
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: API Client (SDK) for DANA APIs based on https://dashboard.dana.id/api-docs
|
|
5
5
|
Author-email: DANA Package Manager <package-manager@dana.id>
|
|
6
6
|
Maintainer-email: DANA Package Manager <package-manager@dana.id>
|
|
@@ -12,9 +12,9 @@ Requires-Python: >3.9.1
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: annotated-types==0.7.0
|
|
15
|
-
Requires-Dist: cffi==
|
|
15
|
+
Requires-Dist: cffi==2.0.0
|
|
16
16
|
Requires-Dist: cryptography<46.0.0,>=44.0.2
|
|
17
|
-
Requires-Dist: pycparser==2.
|
|
17
|
+
Requires-Dist: pycparser==2.23
|
|
18
18
|
Requires-Dist: pydantic<3.0.0,>=2.10.6
|
|
19
19
|
Requires-Dist: pydantic-core<3.0.0,>=2.27.2
|
|
20
20
|
Requires-Dist: python-dateutil==2.9.0.post0
|
|
@@ -88,7 +88,7 @@ Before using the SDK, please make sure to set the following environment variable
|
|
|
88
88
|
| ---------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
89
89
|
| `ENV` or `DANA_ENV` | Defines which environment the SDK will use. Possible values: `SANDBOX` or `PRODUCTION`. | `SANDBOX` |
|
|
90
90
|
| `X_PARTNER_ID` | Unique identifier for partner, provided by DANA, also known as `clientId`. | 1970010100000000000000 |
|
|
91
|
-
| `PRIVATE_KEY` | Your private key string. |
|
|
91
|
+
| `PRIVATE_KEY` | Your private key string. | |
|
|
92
92
|
| `PRIVATE_KEY_PATH` | Path to your private key file. If both are set, `PRIVATE_KEY_PATH` is used. | /path/to/your_private_key.pem |
|
|
93
93
|
| `DANA_PUBLIC_KEY` | DANA public key string for parsing webhook. | `-----BEGIN PUBLIC KEY-----MIIBIjANBgkq...Do/QIDAQAB-----END PUBLIC KEY-----` |
|
|
94
94
|
| `DANA_PUBLIC_KEY_PATH` | Path to DANA public key file for parsing webhook. If both set, `DANA_PUBLIC_KEY_PATH is used. | /path/to/dana_public_key.pem |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
dana/__init__.py,sha256=_1fR4wVGZVf1d5G_ogKEEQkJ2fc6z_Zv4qDTlALTCHg,593
|
|
2
2
|
dana/api_client.py,sha256=Rn8Wyuz77U4A9uI_2wfOsasWtT8BqKxNl0XSC6dVYgA,28653
|
|
3
3
|
dana/api_response.py,sha256=J58dA1ZxuIRYq9ZaK6xJRUdtwAvdOfB3J_hy6QV3KcM,1245
|
|
4
|
-
dana/exceptions.py,sha256=
|
|
4
|
+
dana/exceptions.py,sha256=DD075byy8lQC56nP6x1S7rc-kSab9OBb9tS84OPQutQ,7176
|
|
5
5
|
dana/rest.py,sha256=XvaqswSR6cYFiKj6n-qNUenYjB2FbjqlyG9tuo2-lqM,10268
|
|
6
6
|
dana/base/__init__.py,sha256=bMn0d6EvbwtmxNyZya0vkccpsMwofvp0LvA_siJ0pxI,593
|
|
7
7
|
dana/base/configuration.py,sha256=dVMKHdZ94rlP1IErcis29oE7Nvr4ewrCMGh-3lKcc64,18410
|
|
@@ -11,7 +11,7 @@ dana/disbursement/__init__.py,sha256=_1fR4wVGZVf1d5G_ogKEEQkJ2fc6z_Zv4qDTlALTCHg
|
|
|
11
11
|
dana/disbursement/v1/__init__.py,sha256=C981Ksji-mo9bOhFhI-9g0upO279XZ1sn8a6GIHkU-k,2983
|
|
12
12
|
dana/disbursement/v1/enum.py,sha256=yRZLRYwyvzfg06XJhKTPbIZT8__IzPMgzDRnknCM-lA,861
|
|
13
13
|
dana/disbursement/v1/api/__init__.py,sha256=g22N5qjTc2jl1wsbCydP1g7cqWBthEaOB5g4SWlp6_c,711
|
|
14
|
-
dana/disbursement/v1/api/disbursement_api.py,sha256=
|
|
14
|
+
dana/disbursement/v1/api/disbursement_api.py,sha256=6I5liei3pVDM02wUMvQYTvT82X3Xj0_M1wPPjyEvksc,75191
|
|
15
15
|
dana/disbursement/v1/models/__init__.py,sha256=eCiQWHiKPdQvcbAz7kclRMGoHL4qMIm2DPakBtGxVm4,2858
|
|
16
16
|
dana/disbursement/v1/models/bank_account_inquiry_request.py,sha256=-qK599Biioyhn-RMUxq02hnzT2qkYOGEuFLDxIaOfJk,5431
|
|
17
17
|
dana/disbursement/v1/models/bank_account_inquiry_request_additional_info.py,sha256=cDP4HFvHWlOp6ZWZ00JTF1kbBAz2om7Kpvvx0ADylzE,6028
|
|
@@ -83,7 +83,7 @@ dana/merchant_management/v1/models/update_shop_response_response_head.py,sha256=
|
|
|
83
83
|
dana/merchant_management/v1/models/user_name.py,sha256=eRNJu5iVsMZZdU7nA9QA1g_4RV-bxxGy4MGIBNbXevs,4366
|
|
84
84
|
dana/payment_gateway/__init__.py,sha256=_1fR4wVGZVf1d5G_ogKEEQkJ2fc6z_Zv4qDTlALTCHg,593
|
|
85
85
|
dana/payment_gateway/v1/__init__.py,sha256=cJRNM80F042f3ZsUXeP5t9wTajH5NDBbpnXSV5y_k4E,4243
|
|
86
|
-
dana/payment_gateway/v1/custom_validation.py,sha256=
|
|
86
|
+
dana/payment_gateway/v1/custom_validation.py,sha256=GAWbY2tjVtraBQih1ImkFRYlvcv3qZ25RffhY_fLKVI,12312
|
|
87
87
|
dana/payment_gateway/v1/enum.py,sha256=PXgJwvF2UXXxTPTAJiN5kV-z2kHYWplpXDGd3u3LzRk,2623
|
|
88
88
|
dana/payment_gateway/v1/api/__init__.py,sha256=xYZF0wAVxZ7tn-36ofOKJWEJzc-u4ixG8-gKbGK2Bng,942
|
|
89
89
|
dana/payment_gateway/v1/api/payment_gateway_api.py,sha256=oLu6Y1WtV7qSyXuaQCtgfR_oSdeyevMyXTtp-2RVCGY,63315
|
|
@@ -131,7 +131,7 @@ dana/utils/date_validation.py,sha256=W3t5OH98jf2EJ8PhcTOSNNUWXoH8ykaSd7S76jY6bBI
|
|
|
131
131
|
dana/utils/models.py,sha256=5MYKKkZjO76k-hmrlK8S_LR1jbFRHQBAfZKmyMciX0I,1718
|
|
132
132
|
dana/utils/open_api_configuration.py,sha256=IQfn3c7Zs4dqOOrm1tKWBoTRdCus2alFnzRL3Yjf5ec,14805
|
|
133
133
|
dana/utils/open_api_header.py,sha256=uYUpmnnO5PI2tu30LNx3hE6fI-NOc2VMkc1P3G9xZpw,9537
|
|
134
|
-
dana/utils/script.py,sha256=
|
|
134
|
+
dana/utils/script.py,sha256=zel7LB6GmIGIFyaegYdtmGZj-0tcwe6xuTXvGz-8vLw,2480
|
|
135
135
|
dana/utils/snap_configuration.py,sha256=MZy4dBydo1PqvcB8XVVTBysWdoPmQ8ct7Gzp7jIMFQA,21531
|
|
136
136
|
dana/utils/snap_header.py,sha256=fCmUJlwpb-cvkPTE3MseKNEeVRPVSJXixHxqFrAuBUU,13637
|
|
137
137
|
dana/utils/url.py,sha256=wCDthoodIDANoth6SqSoiIdNwc-hJLNjFBdf_fSjU_4,1965
|
|
@@ -146,7 +146,7 @@ dana/webhook/shop_info.py,sha256=LtPO10Jhlq2K70AiFUB2I-YSKR-mU9m_6OvvCgalXxY,476
|
|
|
146
146
|
dana/webhook/webhook.py,sha256=Pi0rJOG4ogrxXcqEdYWPBPL7iCXoY--LEo_YtqRRLwI,9569
|
|
147
147
|
dana/widget/__init__.py,sha256=_1fR4wVGZVf1d5G_ogKEEQkJ2fc6z_Zv4qDTlALTCHg,593
|
|
148
148
|
dana/widget/v1/__init__.py,sha256=ibJjQQ3NDAK_UwYZKHZzBVAScX4mkQixi_LD3RGTr4U,5576
|
|
149
|
-
dana/widget/v1/custom_validation.py,sha256=
|
|
149
|
+
dana/widget/v1/custom_validation.py,sha256=KDaglDbKrFksCq7bSlgTqUxWeILGavGfFYLXxXt8_r8,3802
|
|
150
150
|
dana/widget/v1/enum.py,sha256=LhI4IjMAb9mm0DB--L86KFWAoWcMXJK-4vCpLLu5yeQ,3443
|
|
151
151
|
dana/widget/v1/util.py,sha256=qxoYnUij5x3QmXb3R8JmcaoJCTnvMcFgGtEUqxITCbA,11250
|
|
152
152
|
dana/widget/v1/api/__init__.py,sha256=0M_z47tKg4vjaHIDSSE7f9COYZd1KmxVl40ZMi1kjog,922
|
|
@@ -209,8 +209,8 @@ dana/widget/v1/models/virtual_account_info.py,sha256=qCFyTqwOFY0KwawiQhRfWYwv0Hv
|
|
|
209
209
|
dana/widget/v1/models/widget_payment_request.py,sha256=KZPOauNqtDvtETLSAgkfgLagOiX2H5KaOGroSbRKCAg,8469
|
|
210
210
|
dana/widget/v1/models/widget_payment_request_additional_info.py,sha256=Qa4Akl-KxeBdh3ITnb0_LxVmgZA_YJsYgW_MHKTHj1A,6273
|
|
211
211
|
dana/widget/v1/models/widget_payment_response.py,sha256=KNfxpyOeNjyRG8aNRzTCaY1W5i0PgSVj3x2O8I6b_kE,6037
|
|
212
|
-
dana_python-2.1.
|
|
213
|
-
dana_python-2.1.
|
|
214
|
-
dana_python-2.1.
|
|
215
|
-
dana_python-2.1.
|
|
216
|
-
dana_python-2.1.
|
|
212
|
+
dana_python-2.1.2.dist-info/licenses/LICENSE,sha256=7CXCr_1HV_P6dPKoffVsU3lk2eVjnRacBDnQIeJMgFc,10232
|
|
213
|
+
dana_python-2.1.2.dist-info/METADATA,sha256=UaXAzPy-Ar2Eg1ADNdPklqIXC9hU-T_fwSLLAkTXu9s,5568
|
|
214
|
+
dana_python-2.1.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
215
|
+
dana_python-2.1.2.dist-info/top_level.txt,sha256=uvbw-Siay0DC-rXYYx11_k0lqDnrOl5tFeSkE-3Mb8I,5
|
|
216
|
+
dana_python-2.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|