scythe-ttp 0.12.4__py3-none-any.whl → 0.14.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.

Potentially problematic release.


This version of scythe-ttp might be problematic. Click here for more details.

scythe/auth/__init__.py CHANGED
@@ -8,9 +8,11 @@ authenticate before executing their main functionality.
8
8
  from .base import Authentication
9
9
  from .bearer import BearerTokenAuth
10
10
  from .basic import BasicAuth
11
+ from .cookie_jwt import CookieJWTAuth
11
12
 
12
13
  __all__ = [
13
14
  'Authentication',
14
15
  'BearerTokenAuth',
15
- 'BasicAuth'
16
+ 'BasicAuth',
17
+ 'CookieJWTAuth',
16
18
  ]
scythe/auth/base.py CHANGED
@@ -80,6 +80,15 @@ class Authentication(ABC):
80
80
  """
81
81
  return {}
82
82
 
83
+ def get_auth_cookies(self) -> Dict[str, str]:
84
+ """
85
+ Get authentication cookies that should be set for API requests.
86
+
87
+ Returns:
88
+ Dictionary mapping cookie name to cookie value.
89
+ """
90
+ return {}
91
+
83
92
  def store_auth_data(self, key: str, value: Any) -> None:
84
93
  """
85
94
  Store authentication-related data.
@@ -0,0 +1,172 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Dict, Optional, Any
5
+ from urllib.parse import urlparse
6
+
7
+ try:
8
+ import requests # type: ignore
9
+ except Exception: # pragma: no cover - tests may run without requests installed
10
+ requests = None # type: ignore
11
+ from selenium.webdriver.remote.webdriver import WebDriver
12
+
13
+ from .base import Authentication, AuthenticationError
14
+
15
+
16
+ def _extract_by_dot_path(data: Any, path: str) -> Optional[Any]:
17
+ """
18
+ Extract a value from a nested dict/list structure using a simple dot path.
19
+ Supports numeric indices for lists, e.g., "data.items.0.token".
20
+ """
21
+ if not path:
22
+ return None
23
+ parts = path.split(".")
24
+ current: Any = data
25
+ for part in parts:
26
+ if isinstance(current, dict):
27
+ if part in current:
28
+ current = current[part]
29
+ else:
30
+ return None
31
+ elif isinstance(current, list):
32
+ try:
33
+ idx = int(part)
34
+ except ValueError:
35
+ return None
36
+ if idx < 0 or idx >= len(current):
37
+ return None
38
+ current = current[idx]
39
+ else:
40
+ return None
41
+ return current
42
+
43
+
44
+ class CookieJWTAuth(Authentication):
45
+ """
46
+ Hybrid authentication where a JWT is acquired from an API login response and
47
+ then used as a cookie for subsequent requests. Useful when the target server
48
+ expects a cookie (e.g., "stellarbridge") instead of Authorization headers.
49
+
50
+ Behavior:
51
+ - In API mode: JourneyExecutor will call get_auth_cookies(); this class will
52
+ perform a POST to login_url (if token not cached), parse JSON, extract the
53
+ token via jwt_json_path, and return {cookie_name: token}.
54
+ - In UI mode: authenticate() will ensure the browser has the cookie set for
55
+ the target domain.
56
+ """
57
+
58
+ def __init__(self,
59
+ login_url: str,
60
+ username: Optional[str] = None,
61
+ password: Optional[str] = None,
62
+ username_field: str = "email",
63
+ password_field: str = "password",
64
+ extra_fields: Optional[Dict[str, Any]] = None,
65
+ jwt_json_path: str = "token",
66
+ cookie_name: str = "stellarbridge",
67
+ session: Optional[requests.Session] = None,
68
+ description: str = "Authenticate via API and set JWT cookie"):
69
+ super().__init__(
70
+ name="Cookie JWT Authentication",
71
+ description=description
72
+ )
73
+ self.login_url = login_url
74
+ self.username = username
75
+ self.password = password
76
+ self.username_field = username_field
77
+ self.password_field = password_field
78
+ self.extra_fields = extra_fields or {}
79
+ self.jwt_json_path = jwt_json_path
80
+ self.cookie_name = cookie_name
81
+ # Avoid importing requests in test environments; allow injected session
82
+ self._session = session or (requests.Session() if requests is not None else None)
83
+ self.token: Optional[str] = None
84
+
85
+ def _login_and_get_token(self) -> str:
86
+ payload: Dict[str, Any] = dict(self.extra_fields)
87
+ if self.username is not None:
88
+ payload[self.username_field] = self.username
89
+ if self.password is not None:
90
+ payload[self.password_field] = self.password
91
+ try:
92
+ resp = self._session.post(self.login_url, json=payload, timeout=15)
93
+ # try json; raise on non-2xx to surface errors
94
+ resp.raise_for_status()
95
+ data = resp.json()
96
+ except Exception as e:
97
+ raise AuthenticationError(f"Login request failed: {e}", self.name)
98
+ token = _extract_by_dot_path(data, self.jwt_json_path)
99
+ if not token or not isinstance(token, str):
100
+ raise AuthenticationError(
101
+ f"JWT not found at path '{self.jwt_json_path}' in login response",
102
+ self.name,
103
+ )
104
+ self.token = token
105
+ self.store_auth_data('jwt', token)
106
+ self.store_auth_data('login_time', time.time())
107
+ return token
108
+
109
+ def get_auth_cookies(self) -> Dict[str, str]:
110
+ """
111
+ Return cookie mapping for API mode. Will perform login if token absent.
112
+ """
113
+ if not self.token:
114
+ self._login_and_get_token()
115
+ if not self.token:
116
+ return {}
117
+ return {self.cookie_name: self.token}
118
+
119
+ def get_auth_headers(self) -> Dict[str, str]:
120
+ """
121
+ For this hybrid approach, we typically do not use auth headers.
122
+ """
123
+ return {}
124
+
125
+ def authenticate(self, driver: WebDriver, target_url: str) -> bool:
126
+ """
127
+ UI path: ensure the cookie exists on the browser for the target domain.
128
+ Will perform the API login if token not yet acquired.
129
+ """
130
+ try:
131
+ if not self.token:
132
+ self._login_and_get_token()
133
+ if not self.token:
134
+ return False
135
+ # Navigate to the target domain base so cookie domain matches
136
+ parsed = urlparse(target_url)
137
+ base = f"{parsed.scheme}://{parsed.netloc}"
138
+ try:
139
+ driver.get(base)
140
+ except Exception:
141
+ pass
142
+ cookie_dict = {
143
+ 'name': self.cookie_name,
144
+ 'value': self.token,
145
+ 'path': '/',
146
+ }
147
+ # If domain available, set explicitly to be safe
148
+ if parsed.netloc:
149
+ cookie_dict['domain'] = parsed.hostname or parsed.netloc
150
+ driver.add_cookie(cookie_dict)
151
+ self.authenticated = True
152
+ return True
153
+ except Exception as e:
154
+ raise AuthenticationError(f"Cookie auth failed: {e}", self.name)
155
+
156
+ def is_authenticated(self, driver: WebDriver) -> bool:
157
+ return self.authenticated and self.token is not None
158
+
159
+ def logout(self, driver: WebDriver) -> bool:
160
+ try:
161
+ self.token = None
162
+ self.authenticated = False
163
+ self.clear_auth_data()
164
+ # Best-effort cookie removal
165
+ try:
166
+ driver.delete_cookie(self.cookie_name)
167
+ except Exception:
168
+ pass
169
+ super().logout(driver)
170
+ return True
171
+ except Exception:
172
+ return False
scythe/cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .main import main
2
+
3
+ __all__ = ["main"]