dana-python 2.2.1__py3-none-any.whl → 3.0.0__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.
@@ -231,9 +231,9 @@ class SnapConfiguration(BaseConfiguration):
231
231
  ) -> None:
232
232
  """Constructor
233
233
  """
234
- self.host = "http://api.sandbox.dana.id" if (api_key["DANA_ENV"] == Env.SANDBOX or api_key["ENV"] == Env.SANDBOX) else \
234
+ self.host = "https://api.sandbox.dana.id" if (api_key["DANA_ENV"] == Env.SANDBOX or api_key["ENV"] == Env.SANDBOX) else \
235
235
  "https://api.saas.dana.id" if (api_key["DANA_ENV"] == Env.PRODUCTION or api_key["ENV"] == Env.PRODUCTION) else None
236
- self._base_path = "http://api.sandbox.dana.id" if (api_key["DANA_ENV"] == Env.SANDBOX or api_key["ENV"] == Env.SANDBOX) else \
236
+ self._base_path = "https://api.sandbox.dana.id" if (api_key["DANA_ENV"] == Env.SANDBOX or api_key["ENV"] == Env.SANDBOX) else \
237
237
  "https://api.saas.dana.id" if (api_key["DANA_ENV"] == Env.PRODUCTION or api_key["ENV"] == Env.PRODUCTION) else None
238
238
  """Default Base url
239
239
  """
@@ -595,7 +595,7 @@ class SnapConfiguration(BaseConfiguration):
595
595
 
596
596
  host_settings = [
597
597
  {
598
- 'url': "http://api.sandbox.dana.id",
598
+ 'url': "https://api.sandbox.dana.id",
599
599
  'description': "No description provided",
600
600
  }
601
601
  ] if (self.api_key["DANA_ENV"] == Env.SANDBOX or self.api_key["ENV"] == Env.SANDBOX) else [
dana/webhook/webhook.py CHANGED
@@ -25,7 +25,7 @@ from cryptography.hazmat.primitives.asymmetric import padding
25
25
 
26
26
  from dana.webhook.finish_notify_request import FinishNotifyRequest
27
27
 
28
- # Sandbox gateway public key used for webhook signature verification when DANA_ENV/ENV is sandbox.
28
+ # Sandbox gateway public key used for webhook signature verification only when DANA_ENV/ENV is explicitly sandbox.
29
29
  SANDBOX_WEBHOOK_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
30
30
  MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnaKVGRbin4Wh4KN35OPh
31
31
  ytJBjYTz7QZKSZjmHfiHxFmulfT87rta+IvGJ0rCBgg+1EtKk1hX8G5gPGJs1htJ
@@ -37,9 +37,41 @@ X6JV1OSST1C9sbPGzMHZ8AGB51BM0mok7davD/5irUk+f0C25OgzkwtxAt80dkDo
37
37
  -----END PUBLIC KEY-----"""
38
38
 
39
39
 
40
+ def resolve_webhook_key_content(public_key=None, public_key_path=None) -> str:
41
+ """
42
+ Selects the webhook public key content.
43
+
44
+ The sandbox key is used only when the environment is explicitly sandbox
45
+ (first non-empty of DANA_ENV, ENV, trimmed and lower-cased). In any other
46
+ case, including when neither variable is set, the supplied key is used:
47
+ public_key_path first, then public_key.
48
+
49
+ Raises:
50
+ ValueError: If no key is available or the key file cannot be read.
51
+ """
52
+ env = (os.getenv("DANA_ENV") or os.getenv("ENV") or "").strip().lower()
53
+ if env == "sandbox":
54
+ return SANDBOX_WEBHOOK_PUBLIC_KEY
55
+ if public_key_path:
56
+ try:
57
+ return Path(public_key_path).read_text().strip()
58
+ except Exception as e:
59
+ raise ValueError(f"Failed to read key from file path '{public_key_path}': {e}")
60
+ if public_key:
61
+ return public_key.strip()
62
+ raise ValueError(
63
+ "Either 'public_key' or 'public_key_path' must be provided. "
64
+ "In production, pass your DANA public key via 'public_key' or 'public_key_path'. "
65
+ "Set DANA_ENV=sandbox to use the built-in sandbox key."
66
+ )
67
+
68
+
40
69
  class WebhookParser:
41
70
  """
42
- Verifies incoming webhook signatures and parse webhook request into FinishNotifyRequest object
71
+ Verifies incoming webhook signatures and parse webhook request into FinishNotifyRequest object.
72
+
73
+ The built-in sandbox key is used only when DANA_ENV (or ENV) is explicitly "sandbox";
74
+ otherwise the supplied public_key_path / public_key is required.
43
75
  """
44
76
  def __init__(self, public_key: str = None, public_key_path: str = None):
45
77
  """
@@ -48,23 +80,13 @@ class WebhookParser:
48
80
  public_key (str, optional): The public key as a string. Defaults to None.
49
81
  public_key_path (str, optional): Path to the public key PEM file. Defaults to None.
50
82
  If provided, this will be prioritized over public_key.
83
+ When DANA_ENV (or ENV) is "sandbox", the built-in sandbox key is used and both
84
+ arguments are ignored. For any other value, including unset, one of them is required.
51
85
  Raises:
52
- ValueError: If neither public_key nor public_key_path is provided,
86
+ ValueError: If no key is available (and the environment is not sandbox),
53
87
  if the key file cannot be read, or if the key format is invalid.
54
88
  """
55
- key_input_content = ""
56
- env = (os.getenv("DANA_ENV") or os.getenv("ENV") or "").strip().lower()
57
- if env == "sandbox" or env == "":
58
- key_input_content = SANDBOX_WEBHOOK_PUBLIC_KEY
59
- elif public_key_path:
60
- try:
61
- key_input_content = Path(public_key_path).read_text().strip()
62
- except Exception as e:
63
- raise ValueError(f"Failed to read key from file path '{public_key_path}': {e}")
64
- elif public_key:
65
- key_input_content = public_key.strip()
66
- else:
67
- raise ValueError("Either 'public_key' or 'public_key_path' must be provided.")
89
+ key_input_content = resolve_webhook_key_content(public_key, public_key_path)
68
90
 
69
91
  if not key_input_content:
70
92
  raise ValueError("Key content is empty.")
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, List, Union
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
- # Use environment variable or default to sandbox
81
- env = os.environ.get('DANA_ENV') or os.environ.get('ENV') or 'sandbox'
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 = 'https://m.dana.id/n/link/binding' if env.lower() == 'production' else 'https://m.sandbox.dana.id/n/link/binding'
89
- elif mode == Mode.API:
90
- base_url = 'https://m.dana.id/v1.0/get-auth-code' if env.lower() == 'production' else 'https://m.sandbox.dana.id/v1.0/get-auth-code'
91
-
92
- # Get partner ID from environment
93
- partner_id = os.environ.get('X_PARTNER_ID')
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('X_PARTNER_ID is not defined')
96
-
97
- # Use provided state or generate a new UUID
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
- # Generate channel ID
103
- channel_id = os.environ.get('X_PARTNER_ID')
104
-
105
- # Get scopes based on environment
106
- if hasattr(data, 'scopes') and data.scopes:
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
- if env.lower() != 'production':
110
- scopes = 'CASHIER,AGREEMENT_PAY,QUERY_BALANCE,DEFAULT_BASIC_PROFILE,MINI_DANA'
111
- else:
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
- # Get merchant ID
120
- merchant_id = getattr(data, 'merchant_id', None)
121
-
122
- # Generate timestamp in Jakarta time (UTC+7) with RFC3339 format
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
- 'partnerId': partner_id,
140
- 'scopes': scopes,
141
- 'terminalType': TerminalType.WEB,
142
- 'externalId': external_id,
143
- 'requestId': request_id,
144
- 'redirectUrl': data.redirect_url if hasattr(data, 'redirect_url') else None,
145
- 'state': state,
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
- elif mode == Mode.API:
217
+ else:
148
218
  url_params = {
149
- 'partnerId': partner_id,
150
- 'scopes': scopes,
151
- 'externalId': external_id,
152
- 'channelId': channel_id,
153
- 'redirectUrl': data.redirect_url if hasattr(data, 'redirect_url') else None,
154
- 'timestamp': timestamp,
155
- 'state': state,
156
- 'isSnapBI': 'true'
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
- # Add merchant ID if provided
160
- if merchant_id and mode == Mode.API:
161
- url_params['merchantId'] = merchant_id
162
-
163
- # Add subMerchantId if provided
164
- if hasattr(data, 'sub_merchant_id') and data.sub_merchant_id and mode == Mode.API:
165
- url_params['subMerchantId'] = data.sub_merchant_id
166
-
167
- # Add lang if provided
168
- if hasattr(data, 'lang') and data.lang and mode == Mode.API:
169
- url_params['lang'] = data.lang
170
-
171
- # Add allowRegistration if provided
172
- if hasattr(data, 'allow_registration') and data.allow_registration is not None and mode == Mode.API:
173
- url_params['allowRegistration'] = 'true' if data.allow_registration else 'false'
174
-
175
- # Handle seamless data if provided
176
- seamless_data = getattr(data, 'seamless_data', None)
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
- # Convert object to dict if necessary
179
- if not isinstance(seamless_data, dict):
180
- try:
181
- seamless_data = vars(seamless_data)
182
- except TypeError:
183
- seamless_data = json.loads(json.dumps(seamless_data))
184
-
185
- # Ensure seamless_data is a dict and convert mobileNumber/mobile_number to mobile if needed
186
- if isinstance(seamless_data, dict):
187
- if 'mobileNumber' in seamless_data:
188
- seamless_data['mobile'] = seamless_data['mobileNumber']
189
- del seamless_data['mobileNumber']
190
- if 'mobile_number' in seamless_data:
191
- seamless_data['mobile'] = seamless_data['mobile_number']
192
- del seamless_data['mobile_number']
193
-
194
- if mode == Mode.DEEPLINK:
195
- seamless_data['externalUid'] = getattr(data, 'external_id', '')
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
- else:
244
- # Fallback to a simple hash for testing
245
- return hashlib.sha256(data_to_sign + str(int(datetime.now().timestamp())).encode()).hexdigest()
246
-
247
- except Exception as e:
248
- # For testing purposes, generate a mock signature if key is not available
249
- return hashlib.sha256((json.dumps(seamless_data) + str(int(datetime.now().timestamp()))).encode()).hexdigest()
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, 'web_redirect_url', None)
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, 'user_resources', None)
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], 'value', None) if user_resources else None
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['ott'] = str(ott_value)
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.1
3
+ Version: 3.0.0
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>
@@ -8,19 +8,16 @@ License-Expression: Apache-2.0
8
8
  Project-URL: homepage, https://dashboard.dana.id/api-docs
9
9
  Project-URL: repository, https://github.com/dana-id/dana-python
10
10
  Keywords: DANA,DANA ID Docs,DANA SDK,DANA Python,DANA API Client,DANA Python API Client,DANA Python SDK
11
- Requires-Python: >3.9.1
11
+ Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: annotated-types==0.7.0
15
- Requires-Dist: cffi==2.0.0
16
- Requires-Dist: cryptography<46.0.0,>=44.0.2
17
- Requires-Dist: pycparser==2.23
14
+ Requires-Dist: annotated-types>=0.7.0
15
+ Requires-Dist: cryptography>=50.0.0
18
16
  Requires-Dist: pydantic<3.0.0,>=2.10.6
19
17
  Requires-Dist: pydantic-core<3.0.0,>=2.27.2
20
- Requires-Dist: python-dateutil==2.9.0.post0
21
- Requires-Dist: six==1.17.0
18
+ Requires-Dist: python-dateutil>=2.9.0.post0
22
19
  Requires-Dist: typing-extensions<5.0.0,>=4.12.2
23
- Requires-Dist: urllib3<3.0.0,>=2.3.0
20
+ Requires-Dist: urllib3<3.0.0,>=2.8.0
24
21
  Dynamic: license-file
25
22
 
26
23
  # dana-python
@@ -62,7 +59,7 @@ Here is the link: https://github.com/dana-id/uat-script.
62
59
 
63
60
  ## Requirements.
64
61
 
65
- Python 3.9.1+
62
+ Python 3.10+ (patched releases: 3.11.17, 3.12.15, 3.13.17, 3.14.9, 3.15.1 or later)
66
63
 
67
64
  ## Installation & Usage
68
65
  ### pip install
@@ -152,7 +152,7 @@ dana/utils/models.py,sha256=5MYKKkZjO76k-hmrlK8S_LR1jbFRHQBAfZKmyMciX0I,1718
152
152
  dana/utils/open_api_configuration.py,sha256=IQfn3c7Zs4dqOOrm1tKWBoTRdCus2alFnzRL3Yjf5ec,14805
153
153
  dana/utils/open_api_header.py,sha256=uYUpmnnO5PI2tu30LNx3hE6fI-NOc2VMkc1P3G9xZpw,9537
154
154
  dana/utils/script.py,sha256=zel7LB6GmIGIFyaegYdtmGZj-0tcwe6xuTXvGz-8vLw,2480
155
- dana/utils/snap_configuration.py,sha256=MZy4dBydo1PqvcB8XVVTBysWdoPmQ8ct7Gzp7jIMFQA,21531
155
+ dana/utils/snap_configuration.py,sha256=QBdAELaNQLT-BCRrZbQMaGse-WPMXWZXWJQIckyXTc4,21534
156
156
  dana/utils/snap_header.py,sha256=QE0Kbna1f2WEH5MknLKkWgLNbzdWEjF36DngKBcdXuA,13393
157
157
  dana/utils/url.py,sha256=wCDthoodIDANoth6SqSoiIdNwc-hJLNjFBdf_fSjU_4,1965
158
158
  dana/webhook/__init__.py,sha256=PGG-SGs96pn8aBTVhR9Xg4-DkIvsIRIdZfV57ZG1FwI,626
@@ -163,12 +163,12 @@ dana/webhook/finish_notify_response.py,sha256=9bHXP2TdU02GkOF8F-M6qF6ox-x2wBz6Nr
163
163
  dana/webhook/money.py,sha256=4o0oPAf7LnpVHX45yhqmXs5tg5PKlJ2B-OqfHi4aGQM,3413
164
164
  dana/webhook/pay_option_info.py,sha256=_IA3X04ZIBYsVfoe31w4tYvjz_klKb9GEWDXBoROrF4,8739
165
165
  dana/webhook/shop_info.py,sha256=a7ZsvGA10hao9qeazp5H6jjNt9J_uz9A6cdD5TZSNsU,4722
166
- dana/webhook/webhook.py,sha256=UyI2rOWZYj33kVXpzbjedTxYu2vO4YRUI_-PUVrSNjs,14918
166
+ dana/webhook/webhook.py,sha256=abvxl0pESAvNSrm-cFKjXyquRfvSqrwC97VKTMjVWR0,15900
167
167
  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=4hRFry5ROrhahyQ7hlHNj7TT-lS_-CMH750Ed4jVQLw,11957
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.1.dist-info/licenses/LICENSE,sha256=7CXCr_1HV_P6dPKoffVsU3lk2eVjnRacBDnQIeJMgFc,10232
233
- dana_python-2.2.1.dist-info/METADATA,sha256=NHzFKUBjO0zVQuPtrtsVh4CXNCtLGYbn0O5Z8-mdsE4,5568
234
- dana_python-2.2.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
235
- dana_python-2.2.1.dist-info/top_level.txt,sha256=uvbw-Siay0DC-rXYYx11_k0lqDnrOl5tFeSkE-3Mb8I,5
236
- dana_python-2.2.1.dist-info/RECORD,,
232
+ dana_python-3.0.0.dist-info/licenses/LICENSE,sha256=7CXCr_1HV_P6dPKoffVsU3lk2eVjnRacBDnQIeJMgFc,10232
233
+ dana_python-3.0.0.dist-info/METADATA,sha256=2fhWF0Jl7Bm6DAZbVCbvDY4FNkIU6mrhOjaO3zIOU8k,5545
234
+ dana_python-3.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
235
+ dana_python-3.0.0.dist-info/top_level.txt,sha256=uvbw-Siay0DC-rXYYx11_k0lqDnrOl5tFeSkE-3Mb8I,5
236
+ dana_python-3.0.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (83.0.0)
2
+ Generator: setuptools (84.2.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5