jupyterlite-simple-cors-proxy 0.1.6__py3-none-any.whl → 0.1.8__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,,