jupyterlite-simple-cors-proxy 0.1.6__py3-none-any.whl → 0.1.8__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,7 @@
1
1
  # File: jupyterlite_simple_cors_proxy/__init__.py
2
2
  from .proxy import cors_proxy_get, robust_get_request, xurl, furl
3
3
 
4
- __version__ = "0.1.6"
4
+ # from .fastf1_proxy import enable_cors_proxy as fastf1_cors_proxy
5
+
6
+ __version__ = "0.1.8"
5
7
  __all__ = ["cors_proxy_get", "robust_get_request", "xurl", "furl"]
@@ -0,0 +1,221 @@
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
+ "api.formula1.com",
33
+ "livetiming.formula1.com",
34
+ ]
35
+
36
+ self._setup_logging()
37
+ self._setup_session()
38
+
39
+ def _setup_logging(self) -> None:
40
+ """Configure logging based on debug setting."""
41
+ self.logger = logging.getLogger('CORSProxyPatcher')
42
+ if self.config.debug:
43
+ logging.basicConfig(
44
+ level=logging.DEBUG,
45
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
46
+ )
47
+
48
+ def _setup_session(self) -> None:
49
+ """Set up the requests session with retry functionality."""
50
+ self.session = requests.Session()
51
+ retry_strategy = requests.adapters.Retry(
52
+ total=self.config.retry_count,
53
+ backoff_factor=0.5,
54
+ status_forcelist=[500, 502, 503, 504]
55
+ )
56
+ adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy)
57
+ self.session.mount("http://", adapter)
58
+ self.session.mount("https://", adapter)
59
+
60
+ def should_proxy(self, url: str) -> bool:
61
+ """
62
+ Check if the URL should be routed through proxy based on domain.
63
+
64
+ Args:
65
+ url (str): URL to check
66
+
67
+ Returns:
68
+ bool: True if URL should be proxied
69
+ """
70
+ parsed = urlparse(url)
71
+ should_proxy = any(domain in parsed.netloc for domain in self.domains)
72
+ if self.config.debug:
73
+ self.logger.debug(f"URL: {url} - Should proxy: {should_proxy}")
74
+ return should_proxy
75
+
76
+ def get_proxied_url(self, url: str) -> str:
77
+ """
78
+ Get the proxied version of the URL if needed.
79
+
80
+ Args:
81
+ url (str): Original URL
82
+
83
+ Returns:
84
+ str: Proxied URL if needed, original URL otherwise
85
+ """
86
+ if self.should_proxy(url):
87
+ if 'allorigins' in self.config.proxy_url:
88
+ proxied = f"{self.config.proxy_url}{quote(url, safe='')}"
89
+ else:
90
+ proxied = f"{self.config.proxy_url}{url}"
91
+ if self.config.debug:
92
+ self.logger.debug(f"Original URL: {url}")
93
+ self.logger.debug(f"Proxied URL: {proxied}")
94
+ return proxied
95
+ return url
96
+
97
+ def modify_headers(self, headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
98
+ """
99
+ Modify request headers to handle CORS.
100
+
101
+ Args:
102
+ headers (dict, optional): Original headers
103
+
104
+ Returns:
105
+ dict: Modified headers
106
+ """
107
+ modified_headers = headers.copy() if headers else {}
108
+ modified_headers.update({
109
+ 'Origin': 'null',
110
+ 'Sec-Fetch-Mode': 'cors',
111
+ 'Sec-Fetch-Site': 'cross-site',
112
+ 'Accept': 'application/json, text/plain, */*',
113
+ 'User-Agent': 'Mozilla/5.0 (compatible; FastF1/Python)'
114
+ })
115
+ return modified_headers
116
+
117
+ def log_response(self, response: requests.Response, url: str) -> None:
118
+ """
119
+ Log response details for debugging.
120
+
121
+ Args:
122
+ response (Response): Response object
123
+ url (str): Original URL
124
+ """
125
+ if self.config.debug:
126
+ self.logger.debug(f"\nRequest to: {url}")
127
+ self.logger.debug(f"Status Code: {response.status_code}")
128
+ self.logger.debug(f"Headers: {dict(response.headers)}")
129
+ try:
130
+ self.logger.debug(f"Response Text: {response.text[:500]}...")
131
+ except Exception as e:
132
+ self.logger.debug(f"Couldn't read response text: {e}")
133
+
134
+ def make_request(self, method: str, url: str, headers: Optional[Dict[str, str]] = None,
135
+ **kwargs: Any) -> requests.Response:
136
+ """
137
+ Make an HTTP request with proper error handling and logging.
138
+
139
+ Args:
140
+ method (str): HTTP method ('get' or 'post')
141
+ url (str): URL to request
142
+ headers (dict, optional): Request headers
143
+ **kwargs: Additional request parameters
144
+
145
+ Returns:
146
+ Response: Response object
147
+
148
+ Raises:
149
+ requests.exceptions.RequestException: If request fails
150
+ """
151
+ proxied_url = self.get_proxied_url(url)
152
+ modified_headers = self.modify_headers(headers)
153
+ kwargs['headers'] = modified_headers
154
+ kwargs['timeout'] = kwargs.get('timeout', self.config.timeout)
155
+
156
+ try:
157
+ if fastf1.Cache._requests_session_cached and not fastf1.Cache._tmp_disabled:
158
+ session = fastf1.Cache._requests_session_cached
159
+ else:
160
+ session = self.session
161
+
162
+ response = getattr(session, method)(proxied_url, **kwargs)
163
+ response.raise_for_status()
164
+
165
+ self.log_response(response, url)
166
+ return response
167
+
168
+ except requests.exceptions.RequestException as e:
169
+ if self.config.debug:
170
+ self.logger.error(f"Request failed: {str(e)}")
171
+ raise
172
+
173
+ def patch_fastf1(self) -> None:
174
+ """Patch FastF1's request methods to use CORS proxy."""
175
+ def wrapped_get(cls, url: str, headers: Optional[Dict[str, str]] = None, **kwargs: Any) -> requests.Response:
176
+ return self.make_request('get', url, headers, **kwargs)
177
+
178
+ def wrapped_post(cls, url: str, headers: Optional[Dict[str, str]] = None, **kwargs: Any) -> requests.Response:
179
+ return self.make_request('post', url, headers, **kwargs)
180
+
181
+ fastf1.Cache.requests_get = classmethod(wrapped_get)
182
+ fastf1.Cache.requests_post = classmethod(wrapped_post)
183
+
184
+ def enable_cors_proxy(
185
+ domains: List[str],
186
+ proxy_url: Optional[str] = None,
187
+ debug: bool = False,
188
+ retry_count: int = 3,
189
+ timeout: int = 30
190
+ ) -> CORSProxyPatcher:
191
+ """
192
+ Enable CORS proxy support for FastF1.
193
+
194
+ Args:
195
+ domains (list): List of domains to route through the proxy
196
+ proxy_url (str, optional): Base URL of the CORS proxy service
197
+ debug (bool): Enable debug logging
198
+ retry_count (int): Number of retry attempts for failed requests
199
+ timeout (int): Request timeout in seconds
200
+
201
+ Returns:
202
+ CORSProxyPatcher: Configured proxy patcher instance
203
+ """
204
+ config = ProxyConfig(
205
+ proxy_url=proxy_url or "https://api.allorigins.win/raw?url=",
206
+ domains=domains,
207
+ debug=debug,
208
+ retry_count=retry_count,
209
+ timeout=timeout
210
+ )
211
+
212
+ patcher = CORSProxyPatcher(config)
213
+ patcher.patch_fastf1()
214
+
215
+ return patcher
216
+
217
+ # enable_cors_proxy(
218
+ # domains=["api.formula1.com", "livetiming.formula1.com"],
219
+ # debug=True,
220
+ # proxy_url="https://corsproxy.io/",
221
+ # )
@@ -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.8
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
@@ -23,7 +23,7 @@ Dynamic: requires-python
23
23
  Dynamic: summary
24
24
 
25
25
  # jupyterlite-simple-cors-proxy
26
- Simple CORS proxy for making http requests from JupyterLite
26
+ Simple CORS proxy wrapper for making http requests from JupyterLite. Uses https://corsproxy.io/
27
27
 
28
28
  ## Installation
29
29
 
@@ -0,0 +1,8 @@
1
+ jupyterlite_simple_cors_proxy/__init__.py,sha256=fFGdcTl9-jFFAQJVrb1KXt8dh8NH-JcS79FvAtFT3T4,274
2
+ jupyterlite_simple_cors_proxy/fastf1_proxy.py,sha256=FglRogTIlSJvHOu6lFS3S-EHDb37M93aYjQpoKc1QYs,7614
3
+ jupyterlite_simple_cors_proxy/proxy.py,sha256=hF7SeLx7whBkE8m8bDUaelx2tKfLrSHA_Lp_auFtqeY,1341
4
+ jupyterlite_simple_cors_proxy-0.1.8.dist-info/LICENSE,sha256=Ogw7GUmeZIxmDNiKWsu_N07svNoGnPB7lWyiXHX_rMY,1074
5
+ jupyterlite_simple_cors_proxy-0.1.8.dist-info/METADATA,sha256=VCrtXTTVM4n0BsH-f2siz3s9SeWTDwGcVNJjvvfdz7k,1718
6
+ jupyterlite_simple_cors_proxy-0.1.8.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
7
+ jupyterlite_simple_cors_proxy-0.1.8.dist-info/top_level.txt,sha256=Oh0oQrSmRnBq5u675coiKMbkb2ASg8AGF8ZiZTzUS5Q,30
8
+ jupyterlite_simple_cors_proxy-0.1.8.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,,