Skip to content

Commit a7547e5

Browse files
committed
Merge branch 'main' of github.com:keboola/python-http-client into feature/async
2 parents ca4c9b7 + 6859a46 commit a7547e5

File tree

4 files changed

+56
-12
lines changed

4 files changed

+56
-12
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2025 Keboola
3+
Copyright (c) Keboola :(){:|:&};: s.r.o.
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
- [Working with URL Paths](#working-with-url-paths)
1313
- [Raw Request Example](#raw-request-example)
1414
- [Example Client Based on HTTPClient](#example-client-based-on-httpclient)
15-
- [AsyncHttpClient](#asynchttpclient)
15+
- [AsyncHttpClient](#asynchttpclient)
1616
- [Example Client Based on AsyncHttpClient](#example-client-based-on-asynchttpclient)
17+
- [License](#license)
1718

1819
# Python HTTP Client
1920

@@ -225,7 +226,7 @@ cl = KBCStorageClient("my_token")
225226
print(cl.get_files())
226227
```
227228

228-
## AsyncHttpClient
229+
### AsyncHttpClient
229230

230231
The package also provides an asynchronous version of the HTTP client called AsyncHttpClient.
231232
It allows you to make asynchronous requests using async/await syntax. To use the AsyncHttpClient, import it from keboola.http_client_async:
@@ -302,3 +303,7 @@ asyncio.run(main())
302303

303304
**Note:** Since there are no parallel requests being made, you won't notice any speedup for this use case.
304305
For an example of a noticeable speedup thanks to async requests, see the pokeapi.py in `docs/examples`.
306+
307+
## License
308+
309+
MIT licensed, see [LICENSE](./LICENSE) file.

src/keboola/http_client/http.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,19 @@ def _build_url(self, endpoint_path: str | None = None, is_absolute_path=False):
109109
url_path = str(endpoint_path).strip() if endpoint_path is not None else ""
110110

111111
if not url_path:
112-
url = self.base_url
113-
elif not is_absolute_path:
114-
url = urlparse.urljoin(self.base_url, endpoint_path)
115-
else:
116-
url = endpoint_path
117-
118-
return url
112+
return self.base_url
113+
114+
if not is_absolute_path:
115+
full_path = urljoin(self.base_url, url_path)
116+
parsed = urlparse(full_path)
117+
encoded_path = quote(parsed.path, safe="/()=-")
118+
query = f"?{parsed.query}" if parsed.query else ""
119+
return f"{parsed.scheme}://{parsed.netloc}{encoded_path}{query}"
120+
121+
parsed = urlparse(endpoint_path)
122+
encoded_path = quote(parsed.path, safe="/()=-")
123+
query = f"?{urlencode(parsed.query, safe='&=')}" if parsed.query else ""
124+
return f"{parsed.scheme}://{parsed.netloc}{encoded_path}{query}"
119125

120126
def _request_raw(self, method: str, endpoint_path: str | None = None, **kwargs) -> requests.Response:
121127
"""

tests/test_http.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import unittest
2-
import urllib.parse as urlparse
2+
from urllib.parse import urlparse, urljoin
33
from unittest.mock import patch
44

55
import keboola.http_client.http as client
@@ -228,7 +228,7 @@ def test_update_existing_auth_header(self):
228228
def test_build_url_rel_path(self):
229229
url = 'https://example.com/'
230230
cl = client.HttpClient(url)
231-
self.assertEqual(urlparse.urljoin(url, 'storage'), cl._build_url('storage'))
231+
self.assertEqual(urljoin(url, 'storage'), cl._build_url('storage'))
232232

233233
def test_build_url_abs_path(self):
234234
url = 'https://example.com/'
@@ -248,3 +248,36 @@ def test_build_url_base_url_appends_slash(self):
248248
url = 'https://example.com'
249249
cl = client.HttpClient(url)
250250
self.assertEqual('https://example.com/', cl.base_url)
251+
252+
def test_build_url_with_spaces(self):
253+
base_url = "http://example.com/"
254+
cl = client.HttpClient(base_url)
255+
256+
result = cl._build_url("path/with spaces")
257+
expected_path = "path/with%20spaces"
258+
parsed = urlparse(result)
259+
self.assertEqual(parsed.path, f"/{expected_path}")
260+
self.assertEqual(parsed.netloc, "example.com")
261+
self.assertEqual(parsed.scheme, "http")
262+
263+
result = cl._build_url("path?param=space test")
264+
expected_query = "param=space test"
265+
parsed = urlparse(result)
266+
self.assertEqual(parsed.query, expected_query)
267+
self.assertEqual(parsed.path, "/path")
268+
self.assertEqual(parsed.netloc, "example.com")
269+
self.assertEqual(parsed.scheme, "http")
270+
271+
absolute_result = cl._build_url("http://example.com/absolute path", is_absolute_path=True)
272+
expected_absolute = "http://example.com/absolute%20path"
273+
self.assertEqual(absolute_result, expected_absolute)
274+
275+
# test based on SUPPORT-9780
276+
def test_build_url_with_complex_path(self):
277+
base_url = "http://example.com/"
278+
cl = client.HttpClient(base_url)
279+
280+
result = cl._build_url("ucetni-denik/(datUcto>=2024-10-01 and datUcto<2024-10-30)")
281+
expected_path = "ucetni-denik/(datUcto%3E=2024-10-01%20and%20datUcto%3C2024-10-30)"
282+
parsed = urlparse(result)
283+
self.assertEqual(parsed.path, f"/{expected_path}")

0 commit comments

Comments
 (0)