jupyterlite-simple-cors-proxy 0.1.6__py3-none-any.whl → 0.1.7__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.
@@ -1,5 +1,6 @@
1
1
  # File: jupyterlite_simple_cors_proxy/__init__.py
2
2
  from .proxy import cors_proxy_get, robust_get_request, xurl, furl
3
+ from .fastf1_proxy import enable_cors_proxy as fastf1_cors_proxy
3
4
 
4
- __version__ = "0.1.6"
5
- __all__ = ["cors_proxy_get", "robust_get_request", "xurl", "furl"]
5
+ __version__ = "0.1.7"
6
+ __all__ = ["cors_proxy_get", "robust_get_request", "xurl", "furl", "fastf1_cors_proxy"]
@@ -0,0 +1,212 @@
1
+ #import functools
2
+ from urllib.parse import urlparse, quote
3
+ import requests
4
+ #import requests_cache
5
+ #from requests_cache.session import CachedSession
6
+ import fastf1
7
+ import logging
8
+ from typing import List, Optional, Dict, Any
9
+ from dataclasses import dataclass
10
+
11
+ @dataclass
12
+ class ProxyConfig:
13
+ """Configuration for the CORS proxy."""
14
+ proxy_url: str = "https://api.allorigins.win/raw?url="
15
+ domains: List[str] = None
16
+ debug: bool = False
17
+ retry_count: int = 3
18
+ timeout: int = 30
19
+
20
+ class CORSProxyPatcher:
21
+ """Patches FastF1 to handle CORS requests through a proxy service."""
22
+
23
+ def __init__(self, config: ProxyConfig = None):
24
+ """
25
+ Initialize the CORS proxy patcher for FastF1.
26
+
27
+ Args:
28
+ config (ProxyConfig): Configuration object for the proxy
29
+ """
30
+ self.config = config or ProxyConfig()
31
+ self.domains = self.config.domains or []
32
+
33
+ self._setup_logging()
34
+ self._setup_session()
35
+
36
+ def _setup_logging(self) -> None:
37
+ """Configure logging based on debug setting."""
38
+ self.logger = logging.getLogger('CORSProxyPatcher')
39
+ if self.config.debug:
40
+ logging.basicConfig(
41
+ level=logging.DEBUG,
42
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
43
+ )
44
+
45
+ def _setup_session(self) -> None:
46
+ """Set up the requests session with retry functionality."""
47
+ self.session = requests.Session()
48
+ retry_strategy = requests.adapters.Retry(
49
+ total=self.config.retry_count,
50
+ backoff_factor=0.5,
51
+ status_forcelist=[500, 502, 503, 504]
52
+ )
53
+ adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy)
54
+ self.session.mount("http://", adapter)
55
+ self.session.mount("https://", adapter)
56
+
57
+ def should_proxy(self, url: str) -> bool:
58
+ """
59
+ Check if the URL should be routed through proxy based on domain.
60
+
61
+ Args:
62
+ url (str): URL to check
63
+
64
+ Returns:
65
+ bool: True if URL should be proxied
66
+ """
67
+ parsed = urlparse(url)
68
+ should_proxy = any(domain in parsed.netloc for domain in self.domains)
69
+ if self.config.debug:
70
+ self.logger.debug(f"URL: {url} - Should proxy: {should_proxy}")
71
+ return should_proxy
72
+
73
+ def get_proxied_url(self, url: str) -> str:
74
+ """
75
+ Get the proxied version of the URL if needed.
76
+
77
+ Args:
78
+ url (str): Original URL
79
+
80
+ Returns:
81
+ str: Proxied URL if needed, original URL otherwise
82
+ """
83
+ if self.should_proxy(url):
84
+ if 'allorigins' in self.config.proxy_url:
85
+ proxied = f"{self.config.proxy_url}{quote(url, safe='')}"
86
+ else:
87
+ proxied = f"{self.config.proxy_url}{url}"
88
+ if self.config.debug:
89
+ self.logger.debug(f"Original URL: {url}")
90
+ self.logger.debug(f"Proxied URL: {proxied}")
91
+ return proxied
92
+ return url
93
+
94
+ def modify_headers(self, headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
95
+ """
96
+ Modify request headers to handle CORS.
97
+
98
+ Args:
99
+ headers (dict, optional): Original headers
100
+
101
+ Returns:
102
+ dict: Modified headers
103
+ """
104
+ modified_headers = headers.copy() if headers else {}
105
+ modified_headers.update({
106
+ 'Origin': 'null',
107
+ 'Sec-Fetch-Mode': 'cors',
108
+ 'Sec-Fetch-Site': 'cross-site',
109
+ 'Accept': 'application/json, text/plain, */*',
110
+ 'User-Agent': 'Mozilla/5.0 (compatible; FastF1/Python)'
111
+ })
112
+ return modified_headers
113
+
114
+ def log_response(self, response: requests.Response, url: str) -> None:
115
+ """
116
+ Log response details for debugging.
117
+
118
+ Args:
119
+ response (Response): Response object
120
+ url (str): Original URL
121
+ """
122
+ if self.config.debug:
123
+ self.logger.debug(f"\nRequest to: {url}")
124
+ self.logger.debug(f"Status Code: {response.status_code}")
125
+ self.logger.debug(f"Headers: {dict(response.headers)}")
126
+ try:
127
+ self.logger.debug(f"Response Text: {response.text[:500]}...")
128
+ except Exception as e:
129
+ self.logger.debug(f"Couldn't read response text: {e}")
130
+
131
+ def make_request(self, method: str, url: str, headers: Optional[Dict[str, str]] = None,
132
+ **kwargs: Any) -> requests.Response:
133
+ """
134
+ Make an HTTP request with proper error handling and logging.
135
+
136
+ Args:
137
+ method (str): HTTP method ('get' or 'post')
138
+ url (str): URL to request
139
+ headers (dict, optional): Request headers
140
+ **kwargs: Additional request parameters
141
+
142
+ Returns:
143
+ Response: Response object
144
+
145
+ Raises:
146
+ requests.exceptions.RequestException: If request fails
147
+ """
148
+ proxied_url = self.get_proxied_url(url)
149
+ modified_headers = self.modify_headers(headers)
150
+ kwargs['headers'] = modified_headers
151
+ kwargs['timeout'] = kwargs.get('timeout', self.config.timeout)
152
+
153
+ try:
154
+ if fastf1.Cache._requests_session_cached and not fastf1.Cache._tmp_disabled:
155
+ session = fastf1.Cache._requests_session_cached
156
+ else:
157
+ session = self.session
158
+
159
+ response = getattr(session, method)(proxied_url, **kwargs)
160
+ response.raise_for_status()
161
+
162
+ self.log_response(response, url)
163
+ return response
164
+
165
+ except requests.exceptions.RequestException as e:
166
+ if self.config.debug:
167
+ self.logger.error(f"Request failed: {str(e)}")
168
+ raise
169
+
170
+ def patch_fastf1(self) -> None:
171
+ """Patch FastF1's request methods to use CORS proxy."""
172
+ def wrapped_get(cls, url: str, headers: Optional[Dict[str, str]] = None, **kwargs: Any) -> requests.Response:
173
+ return self.make_request('get', url, headers, **kwargs)
174
+
175
+ def wrapped_post(cls, url: str, headers: Optional[Dict[str, str]] = None, **kwargs: Any) -> requests.Response:
176
+ return self.make_request('post', url, headers, **kwargs)
177
+
178
+ fastf1.Cache.requests_get = classmethod(wrapped_get)
179
+ fastf1.Cache.requests_post = classmethod(wrapped_post)
180
+
181
+ def enable_cors_proxy(
182
+ domains: List[str],
183
+ proxy_url: Optional[str] = None,
184
+ debug: bool = False,
185
+ retry_count: int = 3,
186
+ timeout: int = 30
187
+ ) -> CORSProxyPatcher:
188
+ """
189
+ Enable CORS proxy support for FastF1.
190
+
191
+ Args:
192
+ domains (list): List of domains to route through the proxy
193
+ proxy_url (str, optional): Base URL of the CORS proxy service
194
+ debug (bool): Enable debug logging
195
+ retry_count (int): Number of retry attempts for failed requests
196
+ timeout (int): Request timeout in seconds
197
+
198
+ Returns:
199
+ CORSProxyPatcher: Configured proxy patcher instance
200
+ """
201
+ config = ProxyConfig(
202
+ proxy_url=proxy_url or "https://api.allorigins.win/raw?url=",
203
+ domains=domains,
204
+ debug=debug,
205
+ retry_count=retry_count,
206
+ timeout=timeout
207
+ )
208
+
209
+ patcher = CORSProxyPatcher(config)
210
+ patcher.patch_fastf1()
211
+
212
+ return patcher
@@ -7,6 +7,7 @@ import platform
7
7
  PLATFORM = platform.system().lower()
8
8
 
9
9
  def xurl(url, params=None, force=False):
10
+ """Generate a proxied URL."""
10
11
  if PLATFORM=="emscripten" or force:
11
12
  if params:
12
13
  url = f"{url}?{urlencode(params)}"
@@ -14,15 +15,15 @@ def xurl(url, params=None, force=False):
14
15
 
15
16
  return url
16
17
 
17
- def furl(url, params=None):
18
- """Return file like object."""
19
- r = cors_proxy_get(url, params)
18
+ def furl(url, params=None, force=False):
19
+ """Return file like object after calling the proxied URL."""
20
+ r = cors_proxy_get(url, params, force)
20
21
 
21
22
  # Return a file-like object from the JSON string
22
23
  return io.BytesIO(r.content)
23
24
 
24
25
 
25
- def cors_proxy_get(url, params=None):
26
+ def cors_proxy_get(url, params=None, force=False):
26
27
  """
27
28
  CORS proxy for GET resources with requests-like response.
28
29
 
@@ -33,12 +34,13 @@ def cors_proxy_get(url, params=None):
33
34
  Returns:
34
35
  A requests response object.
35
36
  """
36
- proxy_url = xurl(url, params)
37
+ proxy_url = xurl(url, params, force)
37
38
 
38
39
  # Do a simple requests get and
39
40
  # just pass through the entire response object
40
41
  return requests.get(proxy_url)
41
42
 
43
+
42
44
  def robust_get_request(url, params=None):
43
45
  """
44
46
  Try to make a simple request else fall back to a proxy.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: jupyterlite-simple-cors-proxy
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: A simple CORS proxy utility with requests-like response
5
5
  Home-page: https://github.com/innovationOUtside/jupyterlite-simple-cors-proxy
6
6
  Author: Tony Hirst
@@ -0,0 +1,8 @@
1
+ jupyterlite_simple_cors_proxy/__init__.py,sha256=4IEZXGJ78nCMkOujvGFjwgNf3Rmv77u_lkZWufob5wU,292
2
+ jupyterlite_simple_cors_proxy/fastf1_proxy.py,sha256=zsLspQjuj9gw4qbsFRydZvmfLelXUmb3gUOXjrGzQgs,7505
3
+ jupyterlite_simple_cors_proxy/proxy.py,sha256=hF7SeLx7whBkE8m8bDUaelx2tKfLrSHA_Lp_auFtqeY,1341
4
+ jupyterlite_simple_cors_proxy-0.1.7.dist-info/LICENSE,sha256=Ogw7GUmeZIxmDNiKWsu_N07svNoGnPB7lWyiXHX_rMY,1074
5
+ jupyterlite_simple_cors_proxy-0.1.7.dist-info/METADATA,sha256=oXlGf3Dux_i1RHxUOF5IUOUqNECoZ-4EkLUkTn1UsWc,1682
6
+ jupyterlite_simple_cors_proxy-0.1.7.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
7
+ jupyterlite_simple_cors_proxy-0.1.7.dist-info/top_level.txt,sha256=Oh0oQrSmRnBq5u675coiKMbkb2ASg8AGF8ZiZTzUS5Q,30
8
+ jupyterlite_simple_cors_proxy-0.1.7.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- jupyterlite_simple_cors_proxy/__init__.py,sha256=LLiWTvpFRjtUNxEgpzSWkI4CAXIBtR1XlXgBc-gkkcw,206
2
- jupyterlite_simple_cors_proxy/proxy.py,sha256=d7SF-HszSUA3DemI04UHS8c5bJ2XXshPGD_ngUEe3-s,1236
3
- jupyterlite_simple_cors_proxy-0.1.6.dist-info/LICENSE,sha256=Ogw7GUmeZIxmDNiKWsu_N07svNoGnPB7lWyiXHX_rMY,1074
4
- jupyterlite_simple_cors_proxy-0.1.6.dist-info/METADATA,sha256=aIrUBWiyExB8LxIvsZX5S-TJcXvPw5tM8HIXRiWqEkg,1682
5
- jupyterlite_simple_cors_proxy-0.1.6.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
6
- jupyterlite_simple_cors_proxy-0.1.6.dist-info/top_level.txt,sha256=Oh0oQrSmRnBq5u675coiKMbkb2ASg8AGF8ZiZTzUS5Q,30
7
- jupyterlite_simple_cors_proxy-0.1.6.dist-info/RECORD,,