polars-api 0.1.4__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- examples/test.py +28 -0
- polars_api/__init__.py +3 -0
- polars_api/api.py +91 -0
- polars_api-0.1.4.dist-info/LICENSE +21 -0
- polars_api-0.1.4.dist-info/METADATA +116 -0
- polars_api-0.1.4.dist-info/RECORD +9 -0
- polars_api-0.1.4.dist-info/WHEEL +5 -0
- polars_api-0.1.4.dist-info/top_level.txt +3 -0
- tests/test_api.py +2 -0
examples/test.py
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
import polars as pl
|
2
|
+
|
3
|
+
import polars_api # noqa:F401
|
4
|
+
|
5
|
+
BASE_URL = "https://jsonplaceholder.typicode.com/posts"
|
6
|
+
print(
|
7
|
+
pl.DataFrame({
|
8
|
+
"url": [BASE_URL for _ in range(10)],
|
9
|
+
})
|
10
|
+
.with_columns(
|
11
|
+
pl.struct(
|
12
|
+
userId=3,
|
13
|
+
).alias("params"),
|
14
|
+
pl.struct(
|
15
|
+
title=pl.lit("foo"),
|
16
|
+
body=pl.lit("bar"),
|
17
|
+
userId=pl.arange(10),
|
18
|
+
).alias("body"),
|
19
|
+
)
|
20
|
+
.with_columns(
|
21
|
+
pl.col("url").api.get().str.json_decode().alias("get"),
|
22
|
+
pl.col("url").api.aget().str.json_decode().alias("aget"),
|
23
|
+
pl.col("url").api.get(params=pl.col("params")).str.json_decode().alias("get_params"),
|
24
|
+
pl.col("url").api.post(body=pl.col("body")).str.json_decode().alias("post"),
|
25
|
+
pl.col("url").api.apost(body=pl.col("body")).str.json_decode().alias("apost"),
|
26
|
+
pl.col("url").api.apost(params=pl.col("params")).alias("apost_params"),
|
27
|
+
)
|
28
|
+
)
|
polars_api/__init__.py
ADDED
polars_api/api.py
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
import httpx
|
5
|
+
import polars as pl
|
6
|
+
|
7
|
+
|
8
|
+
def check_status_code(status_code):
|
9
|
+
return status_code >= 200 and status_code < 300
|
10
|
+
|
11
|
+
|
12
|
+
@pl.api.register_expr_namespace("api")
|
13
|
+
class Api:
|
14
|
+
def __init__(self, url: pl.Expr) -> None:
|
15
|
+
self._url = url
|
16
|
+
|
17
|
+
@staticmethod
|
18
|
+
def _get(url: str, params: Optional[dict[str, str]] = None) -> Optional[str]:
|
19
|
+
result = httpx.get(url, params=params)
|
20
|
+
if check_status_code(result.status_code):
|
21
|
+
return result.text
|
22
|
+
else:
|
23
|
+
return None
|
24
|
+
|
25
|
+
@staticmethod
|
26
|
+
def _post(url: str, params: dict[str, str], body: str) -> Optional[str]:
|
27
|
+
result = httpx.post(url, params=params, json=body)
|
28
|
+
if check_status_code(result.status_code):
|
29
|
+
return result.text
|
30
|
+
else:
|
31
|
+
return None
|
32
|
+
|
33
|
+
@staticmethod
|
34
|
+
async def _aget_one(url: str, params: str) -> str:
|
35
|
+
async with httpx.AsyncClient() as client:
|
36
|
+
r = await client.get(url, params=params)
|
37
|
+
return r.text
|
38
|
+
|
39
|
+
async def _aget_all(self, x, params):
|
40
|
+
return await asyncio.gather(*[self._aget_one(url, param) for url, param in zip(x, params)])
|
41
|
+
|
42
|
+
def _aget(self, x, params):
|
43
|
+
return pl.Series(asyncio.run(self._aget_all(x, params)))
|
44
|
+
|
45
|
+
@staticmethod
|
46
|
+
async def _apost_one(url: str, params: str, body: str) -> str:
|
47
|
+
async with httpx.AsyncClient() as client:
|
48
|
+
r = await client.post(url, params=params, json=body)
|
49
|
+
return r.text
|
50
|
+
|
51
|
+
async def _apost_all(self, x, params, body):
|
52
|
+
return await asyncio.gather(*[
|
53
|
+
self._apost_one(url, _params, _body) for url, _params, _body in zip(x, params, body)
|
54
|
+
])
|
55
|
+
|
56
|
+
def _apost(self, x, params, body):
|
57
|
+
return pl.Series(asyncio.run(self._apost_all(x, params, body)))
|
58
|
+
|
59
|
+
def get(self, params: Optional[pl.Expr] = None) -> pl.Expr:
|
60
|
+
if params is None:
|
61
|
+
params = pl.lit(None)
|
62
|
+
return pl.struct(self._url.alias("url"), params.alias("params")).map_elements(
|
63
|
+
lambda x: self._get(x["url"], params=x["params"]),
|
64
|
+
return_dtype=pl.Utf8,
|
65
|
+
)
|
66
|
+
|
67
|
+
def post(self, params: Optional[pl.Expr] = None, body: Optional[pl.Expr] = None) -> pl.Expr:
|
68
|
+
if params is None:
|
69
|
+
params = pl.lit(None)
|
70
|
+
if body is None:
|
71
|
+
body = pl.lit(None)
|
72
|
+
return pl.struct(self._url.alias("url"), params.alias("params"), body.alias("body")).map_elements(
|
73
|
+
lambda x: self._post(x["url"], params=x["params"], body=x["body"]),
|
74
|
+
return_dtype=pl.Utf8,
|
75
|
+
)
|
76
|
+
|
77
|
+
def aget(self, params: Optional[pl.Expr] = None) -> pl.Expr:
|
78
|
+
if params is None:
|
79
|
+
params = pl.lit(None)
|
80
|
+
return pl.struct(self._url.alias("url"), params.alias("params")).map_batches(
|
81
|
+
lambda x: self._aget(x.struct.field("url"), params=x.struct.field("params"))
|
82
|
+
)
|
83
|
+
|
84
|
+
def apost(self, params: Optional[pl.Expr] = None, body: Optional[pl.Expr] = None) -> pl.Expr:
|
85
|
+
if params is None:
|
86
|
+
params = pl.lit(None)
|
87
|
+
if body is None:
|
88
|
+
body = pl.lit(None)
|
89
|
+
return pl.struct(self._url.alias("url"), params.alias("params"), body.alias("body")).map_batches(
|
90
|
+
lambda x: self._apost(x.struct.field("url"), params=x.struct.field("params"), body=x.struct.field("body"))
|
91
|
+
)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Diego Garcia Lozano
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,116 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: polars-api
|
3
|
+
Version: 0.1.4
|
4
|
+
Summary: Polars extension for dealing with REST APIs
|
5
|
+
Author-email: Diego Garcia Lozano <diegoglozano96@gmail.com>
|
6
|
+
Project-URL: Homepage, https://diegoglozano.github.io/polars-api/
|
7
|
+
Project-URL: Repository, https://github.com/diegoglozano/polars-api
|
8
|
+
Project-URL: Documentation, https://diegoglozano.github.io/polars-api/
|
9
|
+
Keywords: python
|
10
|
+
Classifier: Intended Audience :: Developers
|
11
|
+
Classifier: Programming Language :: Python
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
19
|
+
Requires-Python: <4.0,>=3.9
|
20
|
+
Description-Content-Type: text/markdown
|
21
|
+
License-File: LICENSE
|
22
|
+
Requires-Dist: httpx>=0.28.1
|
23
|
+
Requires-Dist: polars>=1.19.0
|
24
|
+
|
25
|
+
# polars-api
|
26
|
+
|
27
|
+
[![Release](https://img.shields.io/github/v/release/diegoglozano/polars-api)](https://img.shields.io/github/v/release/diegoglozano/polars-api)
|
28
|
+
[![Build status](https://img.shields.io/github/actions/workflow/status/diegoglozano/polars-api/main.yml?branch=main)](https://github.com/diegoglozano/polars-api/actions/workflows/main.yml?query=branch%3Amain)
|
29
|
+
[![codecov](https://codecov.io/gh/diegoglozano/polars-api/branch/main/graph/badge.svg)](https://codecov.io/gh/diegoglozano/polars-api)
|
30
|
+
[![Commit activity](https://img.shields.io/github/commit-activity/m/diegoglozano/polars-api)](https://img.shields.io/github/commit-activity/m/diegoglozano/polars-api)
|
31
|
+
[![License](https://img.shields.io/github/license/diegoglozano/polars-api)](https://img.shields.io/github/license/diegoglozano/polars-api)
|
32
|
+
|
33
|
+
Polars extension for dealing with REST APIs
|
34
|
+
|
35
|
+
- **Github repository**: <https://github.com/diegoglozano/polars-api/>
|
36
|
+
- **Documentation** <https://diegoglozano.github.io/polars-api/>
|
37
|
+
|
38
|
+
## Installation
|
39
|
+
|
40
|
+
```sh
|
41
|
+
uv add polars-api
|
42
|
+
```
|
43
|
+
|
44
|
+
```sh
|
45
|
+
poetry add polars-api
|
46
|
+
```
|
47
|
+
|
48
|
+
```sh
|
49
|
+
pip install polars-api
|
50
|
+
```
|
51
|
+
|
52
|
+
## Usage
|
53
|
+
|
54
|
+
Just import the library as `import polars_api` and the new `api` namespace will be available.
|
55
|
+
|
56
|
+
In the following example:
|
57
|
+
|
58
|
+
- We set a base URL using [jsonplaceholder](https://jsonplaceholder.typicode.com/) as a fake REST API
|
59
|
+
- For each row, we generate a different body using a `struct` type
|
60
|
+
- Finally, we call different methods for getting the data:
|
61
|
+
- `.api.get()`: sync GET
|
62
|
+
- `.api.aget()`: async GET
|
63
|
+
- `.api.post()`: sync POST
|
64
|
+
- `.api.apost()`: async POST
|
65
|
+
|
66
|
+
These methods will return the result as a `string`, but with polars you can convert it easily in a struct and access its values using `.str.json_decode()` method.
|
67
|
+
|
68
|
+
```python
|
69
|
+
import polars as pl
|
70
|
+
import polars_api
|
71
|
+
|
72
|
+
|
73
|
+
BASE_URL = "https://jsonplaceholder.typicode.com/posts"
|
74
|
+
df = (
|
75
|
+
pl
|
76
|
+
.DataFrame({
|
77
|
+
"url": [BASE_URL for _ in range(10)],
|
78
|
+
})
|
79
|
+
.with_columns(
|
80
|
+
pl
|
81
|
+
.struct(
|
82
|
+
title=pl.lit("foo"),
|
83
|
+
body=pl.lit("bar"),
|
84
|
+
userId=pl.arange(10),
|
85
|
+
)
|
86
|
+
.alias("body"),
|
87
|
+
)
|
88
|
+
.with_columns(
|
89
|
+
pl
|
90
|
+
.col("url")
|
91
|
+
.api.get()
|
92
|
+
.str.json_decode()
|
93
|
+
.alias("get"),
|
94
|
+
pl
|
95
|
+
.col("url")
|
96
|
+
.api.aget()
|
97
|
+
.str.json_decode()
|
98
|
+
.alias("aget"),
|
99
|
+
pl
|
100
|
+
.col("url")
|
101
|
+
.api.post(body=pl.col("body"))
|
102
|
+
.str.json_decode()
|
103
|
+
.alias("post"),
|
104
|
+
pl
|
105
|
+
.col("url")
|
106
|
+
.api.apost(body=pl.col("body"))
|
107
|
+
.str.json_decode()
|
108
|
+
.alias("apost"),
|
109
|
+
)
|
110
|
+
)
|
111
|
+
|
112
|
+
```
|
113
|
+
|
114
|
+
---
|
115
|
+
|
116
|
+
Repository initiated with [fpgmaas/cookiecutter-uv](https://github.com/fpgmaas/cookiecutter-uv).
|
@@ -0,0 +1,9 @@
|
|
1
|
+
examples/test.py,sha256=e-4VmxKdfRJZ01UN_3IAyAdnENTDGSZsrhGzJOtHwh0,927
|
2
|
+
polars_api/__init__.py,sha256=JoRS_iy8pZLEz_ADKooXOqOPlUQcd5wvdcSa_PMuGLs,40
|
3
|
+
polars_api/api.py,sha256=XjGJu4Hhg_7aN_FmMqeH953sD7V1l1OS2F5JcNKJBaY,3341
|
4
|
+
tests/test_api.py,sha256=1OfyzFM1fUecl8TnY9CUkWt_zpUNgoIYnN_Ma9ZN77w,32
|
5
|
+
polars_api-0.1.4.dist-info/LICENSE,sha256=Ms_a-6jJtWdrIBCOaYS_hKFCODG2QXHcNgsQJnbujnA,1076
|
6
|
+
polars_api-0.1.4.dist-info/METADATA,sha256=8Vra2wJdHNE-h1MgOI8rNTUmT9pFDOD9VyRbyKaFp8U,3703
|
7
|
+
polars_api-0.1.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
8
|
+
polars_api-0.1.4.dist-info/top_level.txt,sha256=irGgxpseyd3aPqxnvE54AITFumMlSLGg1O9-zg5Gpac,26
|
9
|
+
polars_api-0.1.4.dist-info/RECORD,,
|
tests/test_api.py
ADDED