dana-python 2.2.1__py3-none-any.whl → 2.2.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/widget/v1/util.py +203 -154
- {dana_python-2.2.1.dist-info → dana_python-2.2.2.dist-info}/METADATA +1 -1
- {dana_python-2.2.1.dist-info → dana_python-2.2.2.dist-info}/RECORD +6 -6
- {dana_python-2.2.1.dist-info → dana_python-2.2.2.dist-info}/WHEEL +1 -1
- {dana_python-2.2.1.dist-info → dana_python-2.2.2.dist-info}/licenses/LICENSE +0 -0
- {dana_python-2.2.1.dist-info → dana_python-2.2.2.dist-info}/top_level.txt +0 -0
dana/widget/v1/util.py
CHANGED
|
@@ -31,7 +31,7 @@ import base64
|
|
|
31
31
|
import hashlib
|
|
32
32
|
import urllib.parse
|
|
33
33
|
from datetime import datetime, timezone, timedelta
|
|
34
|
-
from typing import Optional, Dict, Any
|
|
34
|
+
from typing import Optional, Dict, Any
|
|
35
35
|
from dana.widget.v1.models import WidgetPaymentResponse, ApplyOTTResponse
|
|
36
36
|
from dana.utils.snap_header import SnapHeader
|
|
37
37
|
|
|
@@ -52,7 +52,7 @@ except ImportError:
|
|
|
52
52
|
class Mode:
|
|
53
53
|
API = "API"
|
|
54
54
|
DEEPLINK = "DEEPLINK"
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
class TerminalType:
|
|
57
57
|
WEB = "WEB"
|
|
58
58
|
|
|
@@ -61,223 +61,272 @@ class Util:
|
|
|
61
61
|
"""
|
|
62
62
|
Utility class for the Dana Widget API.
|
|
63
63
|
"""
|
|
64
|
-
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
_OAUTH_QUERY_ESCAPE_KEYS = frozenset(
|
|
66
|
+
{"redirectUrl", "timestamp", "seamlessData", "seamlessSign"}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def generate_channel_id() -> str:
|
|
71
|
+
"""Channel ID for widget OAuth URLs (max 5 chars). Matches Go GenerateChannelId."""
|
|
72
|
+
channel_id = os.environ.get("CHANNEL_ID") or "95221"
|
|
73
|
+
return channel_id[:5]
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def generate_scopes() -> str:
|
|
77
|
+
"""OAuth scopes based on environment. Matches Go GenerateScopes."""
|
|
78
|
+
env = os.environ.get("ENV") or os.environ.get("DANA_ENV") or "sandbox"
|
|
79
|
+
if env.lower() != "production":
|
|
80
|
+
return "CASHIER,AGREEMENT_PAY,QUERY_BALANCE,DEFAULT_BASIC_PROFILE,MINI_DANA"
|
|
81
|
+
return "MINI_DANA,CASHIER,QUERY_BALANCE,DEFAULT_BASIC_PROFILE"
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def generate_timestamp() -> str:
|
|
85
|
+
"""Timestamp in Asia/Jakarta RFC3339 format. Matches Go GenerateTimestamp."""
|
|
86
|
+
try:
|
|
87
|
+
from zoneinfo import ZoneInfo
|
|
88
|
+
|
|
89
|
+
now = datetime.now(ZoneInfo("Asia/Jakarta"))
|
|
90
|
+
except Exception:
|
|
91
|
+
now = datetime.now(timezone.utc) + timedelta(hours=7)
|
|
92
|
+
return now.strftime("%Y-%m-%dT%H:%M:%S+07:00")
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _get_attr(data, name: str, default=None):
|
|
96
|
+
if isinstance(data, dict):
|
|
97
|
+
return data.get(name, default)
|
|
98
|
+
return getattr(data, name, default)
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _is_field_set(data, field_name: str) -> bool:
|
|
102
|
+
fields_set = getattr(data, "model_fields_set", None)
|
|
103
|
+
if fields_set is None:
|
|
104
|
+
fields_set = getattr(data, "__fields_set__", None)
|
|
105
|
+
if fields_set is not None:
|
|
106
|
+
return field_name in fields_set
|
|
107
|
+
if isinstance(data, dict):
|
|
108
|
+
return field_name in data
|
|
109
|
+
return getattr(data, field_name, None) is not None
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _normalize_seamless_data(seamless_data) -> Dict[str, Any]:
|
|
113
|
+
if seamless_data is None:
|
|
114
|
+
return {}
|
|
115
|
+
|
|
116
|
+
if isinstance(seamless_data, dict):
|
|
117
|
+
raw = dict(seamless_data)
|
|
118
|
+
elif hasattr(seamless_data, "model_dump"):
|
|
119
|
+
raw = seamless_data.model_dump(exclude_none=True)
|
|
120
|
+
elif hasattr(seamless_data, "dict"):
|
|
121
|
+
raw = seamless_data.dict(exclude_none=True)
|
|
122
|
+
else:
|
|
123
|
+
try:
|
|
124
|
+
raw = vars(seamless_data)
|
|
125
|
+
except TypeError:
|
|
126
|
+
raw = json.loads(json.dumps(seamless_data))
|
|
127
|
+
|
|
128
|
+
result = {
|
|
129
|
+
key: value
|
|
130
|
+
for key, value in raw.items()
|
|
131
|
+
if value is not None and value != ""
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if "mobileNumber" in result:
|
|
135
|
+
result["mobile"] = result.pop("mobileNumber")
|
|
136
|
+
if "mobile_number" in result:
|
|
137
|
+
result["mobile"] = result.pop("mobile_number")
|
|
138
|
+
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def _build_oauth_query_string(url_params: Dict[str, str]) -> str:
|
|
143
|
+
parts = []
|
|
144
|
+
for key, value in url_params.items():
|
|
145
|
+
if value is None:
|
|
146
|
+
continue
|
|
147
|
+
if key in Util._OAUTH_QUERY_ESCAPE_KEYS:
|
|
148
|
+
parts.append(f"{key}={urllib.parse.quote(str(value), safe='')}")
|
|
149
|
+
else:
|
|
150
|
+
parts.append(f"{key}={value}")
|
|
151
|
+
return "&".join(parts)
|
|
152
|
+
|
|
67
153
|
@staticmethod
|
|
68
154
|
def generate_oauth_url(data, private_key: Optional[str] = None, private_key_path: Optional[str] = None) -> str:
|
|
69
155
|
"""
|
|
70
156
|
Generate OAuth URL for testing
|
|
71
|
-
|
|
157
|
+
|
|
72
158
|
Args:
|
|
73
159
|
data: OAuth URL data object
|
|
74
160
|
private_key: Optional private key content
|
|
75
161
|
private_key_path: Optional path to private key file
|
|
76
|
-
|
|
162
|
+
|
|
77
163
|
Returns:
|
|
78
164
|
str: The generated OAuth URL
|
|
79
165
|
"""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# Get mode or default to API
|
|
84
|
-
mode = getattr(data, 'mode', None) or Mode.API
|
|
85
|
-
|
|
86
|
-
# Set base URL based on environment and mode
|
|
166
|
+
env = os.environ.get("DANA_ENV") or os.environ.get("ENV") or "sandbox"
|
|
167
|
+
mode = Util._get_attr(data, "mode") or Mode.API
|
|
168
|
+
|
|
87
169
|
if mode == Mode.DEEPLINK:
|
|
88
|
-
base_url =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
170
|
+
base_url = (
|
|
171
|
+
"https://m.dana.id/n/link/binding"
|
|
172
|
+
if env.lower() == "production"
|
|
173
|
+
else "https://m.sandbox.dana.id/n/link/binding"
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
base_url = (
|
|
177
|
+
"https://m.dana.id/v1.0/get-auth-code"
|
|
178
|
+
if env.lower() == "production"
|
|
179
|
+
else "https://m.sandbox.dana.id/v1.0/get-auth-code"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
partner_id = os.environ.get("X_PARTNER_ID")
|
|
94
183
|
if not partner_id:
|
|
95
|
-
raise RuntimeError(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
state = getattr(data, 'state', None)
|
|
184
|
+
raise RuntimeError("X_PARTNER_ID is not defined")
|
|
185
|
+
|
|
186
|
+
state = Util._get_attr(data, "state")
|
|
99
187
|
if not state:
|
|
100
188
|
state = str(uuid.uuid4())
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
scopes = ','.join(data.scopes)
|
|
189
|
+
|
|
190
|
+
channel_id = Util.generate_channel_id()
|
|
191
|
+
|
|
192
|
+
scopes = Util._get_attr(data, "scopes")
|
|
193
|
+
if scopes:
|
|
194
|
+
scopes = ",".join(scopes) if isinstance(scopes, (list, tuple)) else scopes
|
|
108
195
|
else:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
scopes = 'MINI_DANA,CASHIER,QUERY_BALANCE,DEFAULT_BASIC_PROFILE'
|
|
113
|
-
|
|
114
|
-
# Use provided external ID or generate a UUID
|
|
115
|
-
external_id = getattr(data, 'external_id', None)
|
|
196
|
+
scopes = Util.generate_scopes()
|
|
197
|
+
|
|
198
|
+
external_id = Util._get_attr(data, "external_id")
|
|
116
199
|
if not external_id:
|
|
117
200
|
external_id = str(uuid.uuid4())
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
try:
|
|
124
|
-
# Python's timezone handling is different from PHP
|
|
125
|
-
now = datetime.now(timezone(timedelta(hours=7)))
|
|
126
|
-
timestamp = now.strftime('%Y-%m-%dT%H:%M:%S%z')
|
|
127
|
-
# Format timezone correctly with a colon
|
|
128
|
-
timestamp = timestamp[:-2] + ':' + timestamp[-2:]
|
|
129
|
-
except Exception:
|
|
130
|
-
# Fallback if timezone calculation fails
|
|
131
|
-
now = datetime.now(timezone.utc)
|
|
132
|
-
now += timedelta(hours=7) # Add 7 hours for UTC+7
|
|
133
|
-
timestamp = now.strftime('%Y-%m-%dT%H:%M:%S+07:00')
|
|
134
|
-
|
|
135
|
-
# Build URL with required parameters
|
|
201
|
+
|
|
202
|
+
merchant_id = Util._get_attr(data, "merchant_id") or os.environ.get("MERCHANT_ID", "")
|
|
203
|
+
timestamp = Util.generate_timestamp()
|
|
204
|
+
request_id = None
|
|
205
|
+
|
|
136
206
|
if mode == Mode.DEEPLINK:
|
|
137
207
|
request_id = str(uuid.uuid4())
|
|
138
208
|
url_params = {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
209
|
+
"partnerId": partner_id,
|
|
210
|
+
"scopes": scopes,
|
|
211
|
+
"terminalType": TerminalType.WEB,
|
|
212
|
+
"externalId": external_id,
|
|
213
|
+
"requestId": request_id,
|
|
214
|
+
"redirectUrl": Util._get_attr(data, "redirect_url"),
|
|
215
|
+
"state": state,
|
|
146
216
|
}
|
|
147
|
-
|
|
217
|
+
else:
|
|
148
218
|
url_params = {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
219
|
+
"partnerId": partner_id,
|
|
220
|
+
"scopes": scopes,
|
|
221
|
+
"externalId": external_id,
|
|
222
|
+
"channelId": channel_id,
|
|
223
|
+
"redirectUrl": Util._get_attr(data, "redirect_url"),
|
|
224
|
+
"timestamp": timestamp,
|
|
225
|
+
"state": state,
|
|
226
|
+
"isSnapBI": "true",
|
|
157
227
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
228
|
+
if merchant_id:
|
|
229
|
+
url_params["merchantId"] = merchant_id
|
|
230
|
+
|
|
231
|
+
sub_merchant_id = Util._get_attr(data, "sub_merchant_id")
|
|
232
|
+
if sub_merchant_id:
|
|
233
|
+
url_params["subMerchantId"] = sub_merchant_id
|
|
234
|
+
|
|
235
|
+
if Util._is_field_set(data, "lang"):
|
|
236
|
+
lang = Util._get_attr(data, "lang")
|
|
237
|
+
if lang:
|
|
238
|
+
url_params["lang"] = lang
|
|
239
|
+
|
|
240
|
+
if Util._is_field_set(data, "allow_registration"):
|
|
241
|
+
allow_registration = Util._get_attr(data, "allow_registration")
|
|
242
|
+
if allow_registration is not None:
|
|
243
|
+
url_params["allowRegistration"] = (
|
|
244
|
+
"true" if allow_registration else "false"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
seamless_data = Util._normalize_seamless_data(Util._get_attr(data, "seamless_data"))
|
|
177
248
|
if seamless_data:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
seamless_data['reqTime'] = timestamp
|
|
197
|
-
seamless_data['verifiedTime'] = "0"
|
|
198
|
-
seamless_data['reqMsgId'] = request_id
|
|
199
|
-
|
|
200
|
-
seamless_data_json = json.dumps(seamless_data)
|
|
201
|
-
url_params['seamlessData'] = seamless_data_json
|
|
202
|
-
|
|
203
|
-
url_params['seamlessSign'] = Util.generate_seamless_sign(seamless_data, private_key, private_key_path)
|
|
204
|
-
|
|
205
|
-
# Remove None values
|
|
206
|
-
url_params = {k: v for k, v in url_params.items() if v is not None}
|
|
207
|
-
|
|
208
|
-
# Build the final URL (RFC3986 quote for seamlessData/seamlessSign parity with Go/PHP)
|
|
209
|
-
return base_url + '?' + urllib.parse.urlencode(url_params, quote_via=urllib.parse.quote)
|
|
210
|
-
|
|
249
|
+
if mode == Mode.DEEPLINK and request_id:
|
|
250
|
+
seamless_data["externalUid"] = external_id
|
|
251
|
+
seamless_data["reqTime"] = timestamp
|
|
252
|
+
seamless_data["verifiedTime"] = "0"
|
|
253
|
+
seamless_data["reqMsgId"] = request_id
|
|
254
|
+
|
|
255
|
+
seamless_data_json = json.dumps(seamless_data, separators=(",", ":"))
|
|
256
|
+
url_params["seamlessData"] = seamless_data_json
|
|
257
|
+
|
|
258
|
+
pk = private_key or os.environ.get("PRIVATE_KEY")
|
|
259
|
+
pk_path = private_key_path or os.environ.get("PRIVATE_KEY_PATH")
|
|
260
|
+
seamless_sign = Util.generate_seamless_sign(seamless_data, pk, pk_path)
|
|
261
|
+
if seamless_sign:
|
|
262
|
+
url_params["seamlessSign"] = seamless_sign
|
|
263
|
+
|
|
264
|
+
query_string = Util._build_oauth_query_string(url_params)
|
|
265
|
+
return f"{base_url}?{query_string}"
|
|
266
|
+
|
|
211
267
|
@staticmethod
|
|
212
268
|
def generate_seamless_sign(seamless_data: Dict[str, Any], private_key: Optional[str] = None, private_key_path: Optional[str] = None) -> str:
|
|
213
269
|
"""
|
|
214
270
|
Generate seamless sign for OAuth URL
|
|
215
|
-
|
|
271
|
+
|
|
216
272
|
Args:
|
|
217
273
|
seamless_data: The seamless data to sign
|
|
218
274
|
private_key: Optional private key content
|
|
219
275
|
private_key_path: Optional path to private key file
|
|
220
|
-
|
|
276
|
+
|
|
221
277
|
Returns:
|
|
222
278
|
str: The generated signature
|
|
223
279
|
"""
|
|
224
280
|
try:
|
|
225
|
-
# Get properly formatted private key using SnapHeader utility method
|
|
226
281
|
usable_private_key = SnapHeader.get_usable_private_key(private_key, private_key_path)
|
|
227
|
-
data_to_sign = json.dumps(seamless_data).encode()
|
|
228
|
-
|
|
229
|
-
# Use cryptography library if available
|
|
282
|
+
data_to_sign = json.dumps(seamless_data, separators=(",", ":")).encode()
|
|
283
|
+
|
|
230
284
|
if CRYPTOGRAPHY_AVAILABLE:
|
|
231
|
-
# Load the private key
|
|
232
285
|
key = load_pem_private_key(usable_private_key.encode(), password=None)
|
|
233
|
-
|
|
234
|
-
# Sign the data
|
|
235
286
|
signature = key.sign(
|
|
236
287
|
data_to_sign,
|
|
237
288
|
padding.PKCS1v15(),
|
|
238
|
-
hashes.SHA256()
|
|
289
|
+
hashes.SHA256(),
|
|
239
290
|
)
|
|
240
|
-
|
|
241
|
-
# Return base64 encoded signature
|
|
242
291
|
return base64.b64encode(signature).decode()
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
292
|
+
|
|
293
|
+
return hashlib.sha256(data_to_sign + str(int(datetime.now().timestamp())).encode()).hexdigest()
|
|
294
|
+
|
|
295
|
+
except Exception:
|
|
296
|
+
return hashlib.sha256(
|
|
297
|
+
(json.dumps(seamless_data, separators=(",", ":")) + str(int(datetime.now().timestamp()))).encode()
|
|
298
|
+
).hexdigest()
|
|
299
|
+
|
|
251
300
|
@staticmethod
|
|
252
301
|
def generate_complete_payment_url(widget_payment_response: WidgetPaymentResponse = None, apply_ott_response: ApplyOTTResponse = None) -> str:
|
|
253
302
|
"""
|
|
254
303
|
Combines the webRedirectUrl from WidgetPaymentResponse with the OTT token from ApplyOTTResponse
|
|
255
|
-
|
|
304
|
+
|
|
256
305
|
Args:
|
|
257
306
|
widget_payment_response: The widget payment response
|
|
258
307
|
apply_ott_response: The apply OTT response
|
|
259
|
-
|
|
308
|
+
|
|
260
309
|
Returns:
|
|
261
310
|
str: The generated payment URL or empty string if inputs are invalid
|
|
262
311
|
"""
|
|
263
312
|
if widget_payment_response is None or apply_ott_response is None:
|
|
264
|
-
return
|
|
265
|
-
|
|
266
|
-
web_redirect_url = getattr(widget_payment_response,
|
|
313
|
+
return ""
|
|
314
|
+
|
|
315
|
+
web_redirect_url = getattr(widget_payment_response, "web_redirect_url", None)
|
|
267
316
|
if not web_redirect_url:
|
|
268
|
-
return
|
|
269
|
-
|
|
270
|
-
user_resources = getattr(apply_ott_response,
|
|
317
|
+
return ""
|
|
318
|
+
|
|
319
|
+
user_resources = getattr(apply_ott_response, "user_resources", None)
|
|
271
320
|
if not user_resources or len(user_resources) == 0:
|
|
272
321
|
return web_redirect_url
|
|
273
|
-
|
|
274
|
-
ott_value = getattr(user_resources[0],
|
|
322
|
+
|
|
323
|
+
ott_value = getattr(user_resources[0], "value", None) if user_resources else None
|
|
275
324
|
if not ott_value:
|
|
276
325
|
return web_redirect_url
|
|
277
326
|
|
|
278
327
|
parsed = urllib.parse.urlparse(web_redirect_url)
|
|
279
328
|
q = dict(urllib.parse.parse_qsl(parsed.query, keep_blank_values=True))
|
|
280
|
-
q[
|
|
329
|
+
q["ott"] = str(ott_value)
|
|
281
330
|
new_query = urllib.parse.urlencode(q)
|
|
282
331
|
return urllib.parse.urlunparse((
|
|
283
332
|
parsed.scheme,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dana-python
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.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>
|
|
@@ -168,7 +168,7 @@ dana/widget/__init__.py,sha256=_1fR4wVGZVf1d5G_ogKEEQkJ2fc6z_Zv4qDTlALTCHg,593
|
|
|
168
168
|
dana/widget/v1/__init__.py,sha256=ibJjQQ3NDAK_UwYZKHZzBVAScX4mkQixi_LD3RGTr4U,5576
|
|
169
169
|
dana/widget/v1/custom_validation.py,sha256=XqQLJu3Dh5XdUpG5eXXSq8dYVymha3vDdpkoKjJ0E7U,7028
|
|
170
170
|
dana/widget/v1/enum.py,sha256=-DS06ZaPG_feTR57fy8hzWeFwPAPsunXvqfisfT7HlI,3507
|
|
171
|
-
dana/widget/v1/util.py,sha256=
|
|
171
|
+
dana/widget/v1/util.py,sha256=iMSDdUaLvq0QnjepP_Gor04bO_3HddhxHCyhdjwJ4yA,12588
|
|
172
172
|
dana/widget/v1/api/__init__.py,sha256=0M_z47tKg4vjaHIDSSE7f9COYZd1KmxVl40ZMi1kjog,922
|
|
173
173
|
dana/widget/v1/api/widget_api.py,sha256=WU1Z8-Xo-5oT_PXA5SAJm1pZnYkfKV-VfYNRB_QXp8k,114510
|
|
174
174
|
dana/widget/v1/models/__init__.py,sha256=WpNeouhhZ_NSgCPSnmQ5YYHnQnO7Vo-OKD7gT9gon3s,5469
|
|
@@ -229,8 +229,8 @@ dana/widget/v1/models/virtual_account_info.py,sha256=qCFyTqwOFY0KwawiQhRfWYwv0Hv
|
|
|
229
229
|
dana/widget/v1/models/widget_payment_request.py,sha256=KZPOauNqtDvtETLSAgkfgLagOiX2H5KaOGroSbRKCAg,8469
|
|
230
230
|
dana/widget/v1/models/widget_payment_request_additional_info.py,sha256=Qa4Akl-KxeBdh3ITnb0_LxVmgZA_YJsYgW_MHKTHj1A,6273
|
|
231
231
|
dana/widget/v1/models/widget_payment_response.py,sha256=KNfxpyOeNjyRG8aNRzTCaY1W5i0PgSVj3x2O8I6b_kE,6037
|
|
232
|
-
dana_python-2.2.
|
|
233
|
-
dana_python-2.2.
|
|
234
|
-
dana_python-2.2.
|
|
235
|
-
dana_python-2.2.
|
|
236
|
-
dana_python-2.2.
|
|
232
|
+
dana_python-2.2.2.dist-info/licenses/LICENSE,sha256=7CXCr_1HV_P6dPKoffVsU3lk2eVjnRacBDnQIeJMgFc,10232
|
|
233
|
+
dana_python-2.2.2.dist-info/METADATA,sha256=O2b7-OHIQYqNUTk407-NKW3GqE4v29lWOd_nG6eq3hY,5568
|
|
234
|
+
dana_python-2.2.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
235
|
+
dana_python-2.2.2.dist-info/top_level.txt,sha256=uvbw-Siay0DC-rXYYx11_k0lqDnrOl5tFeSkE-3Mb8I,5
|
|
236
|
+
dana_python-2.2.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|