plantable 0.0.1__tar.gz
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.
- plantable-0.0.1/PKG-INFO +35 -0
- plantable-0.0.1/README.md +57 -0
- plantable-0.0.1/pyproject.toml +7 -0
- plantable-0.0.1/setup.cfg +65 -0
- plantable-0.0.1/src/plantable/__init__.py +9 -0
- plantable-0.0.1/src/plantable/client/__init__.py +4 -0
- plantable-0.0.1/src/plantable/client/account.py +227 -0
- plantable-0.0.1/src/plantable/client/admin.py +817 -0
- plantable-0.0.1/src/plantable/client/base/__init__.py +1 -0
- plantable-0.0.1/src/plantable/client/base/base.py +885 -0
- plantable-0.0.1/src/plantable/client/base/builtin.py +917 -0
- plantable-0.0.1/src/plantable/client/conf.py +12 -0
- plantable-0.0.1/src/plantable/client/core.py +95 -0
- plantable-0.0.1/src/plantable/client/exception.py +2 -0
- plantable-0.0.1/src/plantable/client/user.py +818 -0
- plantable-0.0.1/src/plantable/const.py +22 -0
- plantable-0.0.1/src/plantable/model/__init__.py +4 -0
- plantable-0.0.1/src/plantable/model/account.py +92 -0
- plantable-0.0.1/src/plantable/model/column.py +324 -0
- plantable-0.0.1/src/plantable/model/core.py +391 -0
- plantable-0.0.1/src/plantable/model/event.py +56 -0
- plantable-0.0.1/src/plantable/model/form.py +91 -0
- plantable-0.0.1/src/plantable/scripts.py +46 -0
- plantable-0.0.1/src/plantable/serde/__init__.py +2 -0
- plantable-0.0.1/src/plantable/serde/deserializer/__init__.py +3 -0
- plantable-0.0.1/src/plantable/serde/deserializer/deserializer.py +231 -0
- plantable-0.0.1/src/plantable/serde/deserializer/to_avro.py +100 -0
- plantable-0.0.1/src/plantable/serde/deserializer/to_postgres.py +341 -0
- plantable-0.0.1/src/plantable/serde/deserializer/to_python.py +338 -0
- plantable-0.0.1/src/plantable/serde/serializer/__init__.py +1 -0
- plantable-0.0.1/src/plantable/serde/serializer/from_arrow.py +177 -0
- plantable-0.0.1/src/plantable/serde/serializer/from_python.py +120 -0
- plantable-0.0.1/src/plantable/server/__init__.py +0 -0
- plantable-0.0.1/src/plantable/server/app.py +51 -0
- plantable-0.0.1/src/plantable/server/conf.py +27 -0
- plantable-0.0.1/src/plantable/server/router/__init__.py +1 -0
- plantable-0.0.1/src/plantable/server/router/api_token.py +57 -0
- plantable-0.0.1/src/plantable/server/router/user.py +69 -0
- plantable-0.0.1/src/plantable/server/util.py +126 -0
- plantable-0.0.1/src/plantable/static/__init__.py +0 -0
- plantable-0.0.1/src/plantable/templates.py +76 -0
- plantable-0.0.1/src/plantable/utils.py +69 -0
- plantable-0.0.1/src/plantable.egg-info/PKG-INFO +35 -0
- plantable-0.0.1/src/plantable.egg-info/SOURCES.txt +47 -0
- plantable-0.0.1/src/plantable.egg-info/dependency_links.txt +1 -0
- plantable-0.0.1/src/plantable.egg-info/entry_points.txt +2 -0
- plantable-0.0.1/src/plantable.egg-info/requires.txt +28 -0
- plantable-0.0.1/src/plantable.egg-info/top_level.txt +1 -0
plantable-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plantable
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Author: Woojin Cho
|
|
5
|
+
Author-email: w.cho@cj.net
|
|
6
|
+
License: MIT License
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Requires-Dist: aioboto3
|
|
9
|
+
Requires-Dist: aiohttp==3.9.5
|
|
10
|
+
Requires-Dist: aiokafka
|
|
11
|
+
Requires-Dist: asyncpg
|
|
12
|
+
Requires-Dist: charset-normalizer==2.1.0
|
|
13
|
+
Requires-Dist: click
|
|
14
|
+
Requires-Dist: click-loglevel
|
|
15
|
+
Requires-Dist: dasida
|
|
16
|
+
Requires-Dist: fastapi
|
|
17
|
+
Requires-Dist: fastavro
|
|
18
|
+
Requires-Dist: genson
|
|
19
|
+
Requires-Dist: hiredis
|
|
20
|
+
Requires-Dist: orjson
|
|
21
|
+
Requires-Dist: pandas
|
|
22
|
+
Requires-Dist: parse
|
|
23
|
+
Requires-Dist: pendulum
|
|
24
|
+
Requires-Dist: pyarrow
|
|
25
|
+
Requires-Dist: pydantic==1.10
|
|
26
|
+
Requires-Dist: pypika
|
|
27
|
+
Requires-Dist: python-dotenv
|
|
28
|
+
Requires-Dist: python-multipart
|
|
29
|
+
Requires-Dist: python-socketio<5
|
|
30
|
+
Requires-Dist: redis
|
|
31
|
+
Requires-Dist: requests
|
|
32
|
+
Requires-Dist: sqlalchemy[asyncio]
|
|
33
|
+
Requires-Dist: sqlparse
|
|
34
|
+
Requires-Dist: tabulate
|
|
35
|
+
Requires-Dist: uvicorn
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Plantable
|
|
2
|
+
|
|
3
|
+
SeaTable Python SDK
|
|
4
|
+
|
|
5
|
+
- `client` SeaTable을 제어 - 사용자 및 그룹 관리, 데이터 읽기 및 쓰기 등
|
|
6
|
+
- `server` SeaTable의 데이터를 AWS S3 등으로 전송하는 HTTP Server
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## Client
|
|
11
|
+
|
|
12
|
+
UserClient 사용 예제
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from plantable import UserClient
|
|
16
|
+
|
|
17
|
+
# user client 생성
|
|
18
|
+
uc = UserClient(
|
|
19
|
+
seatable_url="https://seatable.example.com",
|
|
20
|
+
seatable_username="itsme",
|
|
21
|
+
seatable_password="youknownothing"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Workspace 리스트 보기
|
|
25
|
+
await uc.ls()
|
|
26
|
+
|
|
27
|
+
# Workspace 내 Base 리스트 보기
|
|
28
|
+
await uc.ls("my-workspace")
|
|
29
|
+
|
|
30
|
+
# Workspace / Base 내 리스트 보기 (Tables, Views)
|
|
31
|
+
await uc.ls("my-workspace", "some-base")
|
|
32
|
+
|
|
33
|
+
# BaseClient 생성하기 (Table 읽기/쓰기 위해서는 BaseClient 필요)
|
|
34
|
+
bc = await uc.get_base_client_with_account_token("my-workspace", "some-base")
|
|
35
|
+
|
|
36
|
+
# Table 읽기
|
|
37
|
+
tbl = bc.read_table("my-table")
|
|
38
|
+
|
|
39
|
+
# View 읽기
|
|
40
|
+
view = bc.read_view("my-view")
|
|
41
|
+
|
|
42
|
+
# Table 또는 View를 Pandas DataFrame으로 바꾸기
|
|
43
|
+
# 1. Pandas 이용
|
|
44
|
+
import pandas as pd
|
|
45
|
+
df = pd.DataFrame.from_records(tbl)
|
|
46
|
+
|
|
47
|
+
# 2. PyArrow 이용
|
|
48
|
+
import pyarrow as pa
|
|
49
|
+
df = pa.Table.from_pylist(tbl).to_pandas()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## Server
|
|
57
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = plantable
|
|
3
|
+
version = 2026.3.1
|
|
4
|
+
description =
|
|
5
|
+
long_description =
|
|
6
|
+
author = Woojin Cho
|
|
7
|
+
author_email = w.cho@cj.net
|
|
8
|
+
keywords =
|
|
9
|
+
license = MIT License
|
|
10
|
+
|
|
11
|
+
[options]
|
|
12
|
+
dependency_links =
|
|
13
|
+
python_requires = >= 3.8
|
|
14
|
+
package_dir =
|
|
15
|
+
=src
|
|
16
|
+
packages = find:
|
|
17
|
+
install_requires =
|
|
18
|
+
aioboto3
|
|
19
|
+
aiohttp == 3.9.5
|
|
20
|
+
aiokafka
|
|
21
|
+
asyncpg
|
|
22
|
+
charset-normalizer == 2.1.0
|
|
23
|
+
click
|
|
24
|
+
click-loglevel
|
|
25
|
+
dasida
|
|
26
|
+
fastapi
|
|
27
|
+
fastavro
|
|
28
|
+
genson
|
|
29
|
+
hiredis
|
|
30
|
+
orjson
|
|
31
|
+
pandas
|
|
32
|
+
parse
|
|
33
|
+
pendulum
|
|
34
|
+
pyarrow
|
|
35
|
+
pydantic == 1.10
|
|
36
|
+
pypika
|
|
37
|
+
python-dotenv
|
|
38
|
+
python-multipart
|
|
39
|
+
python-socketio < 5
|
|
40
|
+
redis
|
|
41
|
+
requests
|
|
42
|
+
sqlalchemy[asyncio]
|
|
43
|
+
sqlparse
|
|
44
|
+
tabulate
|
|
45
|
+
uvicorn
|
|
46
|
+
include_package_data = True
|
|
47
|
+
|
|
48
|
+
[options.packages.find]
|
|
49
|
+
where = src
|
|
50
|
+
|
|
51
|
+
[options.package_data]
|
|
52
|
+
plantable =
|
|
53
|
+
static/**/*
|
|
54
|
+
static/**/.*
|
|
55
|
+
static/.**/*
|
|
56
|
+
static/.**/.*
|
|
57
|
+
|
|
58
|
+
[options.entry_points]
|
|
59
|
+
console_scripts =
|
|
60
|
+
plantable = plantable.scripts:plantable
|
|
61
|
+
|
|
62
|
+
[egg_info]
|
|
63
|
+
tag_build =
|
|
64
|
+
tag_date = 0
|
|
65
|
+
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import List, Union
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
import orjson
|
|
8
|
+
import requests
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from tabulate import tabulate
|
|
11
|
+
|
|
12
|
+
from ..model import (
|
|
13
|
+
DTABLE_ICON_COLORS,
|
|
14
|
+
DTABLE_ICON_LIST,
|
|
15
|
+
Admin,
|
|
16
|
+
ApiToken,
|
|
17
|
+
Base,
|
|
18
|
+
BaseToken,
|
|
19
|
+
Column,
|
|
20
|
+
Table,
|
|
21
|
+
Team,
|
|
22
|
+
User,
|
|
23
|
+
Webhook,
|
|
24
|
+
)
|
|
25
|
+
from .base import BaseClient
|
|
26
|
+
from .conf import SEATABLE_ACCOUNT_TOKEN, SEATABLE_PASSWORD, SEATABLE_URL, SEATABLE_USERNAME
|
|
27
|
+
from .core import TABULATE_CONF, HttpClient
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
################################################################
|
|
33
|
+
# AccountClient
|
|
34
|
+
################################################################
|
|
35
|
+
class AccountClient(HttpClient):
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
seatable_url: str = SEATABLE_URL,
|
|
39
|
+
seatable_username: str = SEATABLE_USERNAME,
|
|
40
|
+
seatable_password: str = SEATABLE_PASSWORD,
|
|
41
|
+
):
|
|
42
|
+
super().__init__(seatable_url=seatable_url)
|
|
43
|
+
self.username = seatable_username
|
|
44
|
+
self.password = seatable_password
|
|
45
|
+
self.account_token = None
|
|
46
|
+
|
|
47
|
+
self.is_admin = False
|
|
48
|
+
|
|
49
|
+
# do login
|
|
50
|
+
self.login()
|
|
51
|
+
|
|
52
|
+
def login(self):
|
|
53
|
+
auth_url = self.seatable_url + "/api2/auth-token/"
|
|
54
|
+
response = requests.post(auth_url, json={"username": self.username, "password": self.password})
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
results = response.json()
|
|
57
|
+
self.account_token = results["token"]
|
|
58
|
+
|
|
59
|
+
################################################################
|
|
60
|
+
# AUTHENTICATION - API TOKEN
|
|
61
|
+
################################################################
|
|
62
|
+
# [API TOKEN] list api tokens
|
|
63
|
+
async def list_api_tokens(self, workspace_id: str, base_name: str, model: BaseModel = ApiToken):
|
|
64
|
+
"""
|
|
65
|
+
[NOTE]
|
|
66
|
+
workspace id : group = 1 : 1
|
|
67
|
+
"""
|
|
68
|
+
METHOD = "GET"
|
|
69
|
+
URL = f"/api/v2.1/workspace/{workspace_id}/dtable/{base_name}/api-tokens/"
|
|
70
|
+
ITEM = "api_tokens"
|
|
71
|
+
|
|
72
|
+
async with self.session_maker(token=self.account_token) as session:
|
|
73
|
+
response = await self.request(session=session, method=METHOD, url=URL)
|
|
74
|
+
results = response[ITEM]
|
|
75
|
+
|
|
76
|
+
if model:
|
|
77
|
+
results = [model(**x) for x in results]
|
|
78
|
+
|
|
79
|
+
return results
|
|
80
|
+
|
|
81
|
+
# [API TOKEN] create api token
|
|
82
|
+
async def get_or_create_api_token(
|
|
83
|
+
self,
|
|
84
|
+
workspace_id: str,
|
|
85
|
+
base_name: str,
|
|
86
|
+
app_name: str,
|
|
87
|
+
permission: str = "rw",
|
|
88
|
+
model: BaseModel = ApiToken,
|
|
89
|
+
):
|
|
90
|
+
"""
|
|
91
|
+
[NOTE]
|
|
92
|
+
"bad request" returns if app_name is already exists.
|
|
93
|
+
"""
|
|
94
|
+
api_tokens = await self.list_api_tokens(workspace_id=workspace_id, base_name=base_name)
|
|
95
|
+
for api_token in api_tokens:
|
|
96
|
+
if api_token.app_name == app_name:
|
|
97
|
+
return api_token
|
|
98
|
+
|
|
99
|
+
METHOD = "POST"
|
|
100
|
+
URL = f"/api/v2.1/workspace/{workspace_id}/dtable/{base_name}/api-tokens/"
|
|
101
|
+
JSON = {"app_name": app_name, "permission": permission}
|
|
102
|
+
|
|
103
|
+
async with self.session_maker(token=self.account_token) as session:
|
|
104
|
+
response = await self.request(session=session, method=METHOD, url=URL, json=JSON)
|
|
105
|
+
results = model(**response) if model else response
|
|
106
|
+
|
|
107
|
+
return results
|
|
108
|
+
|
|
109
|
+
# [API TOKEN] create temporary api token
|
|
110
|
+
async def create_temp_api_token(self, workspace_id: str, base_name: str, model: BaseModel = ApiToken):
|
|
111
|
+
METHOD = "GET"
|
|
112
|
+
URL = f"/api/v2.1/workspace/{workspace_id}/dtable/{base_name}/temp-api-token/"
|
|
113
|
+
ITEM = "api_token"
|
|
114
|
+
|
|
115
|
+
async with self.session_maker(token=self.account_token) as session:
|
|
116
|
+
response = await self.request(session=session, method=METHOD, url=URL)
|
|
117
|
+
results = response[ITEM]
|
|
118
|
+
|
|
119
|
+
if model:
|
|
120
|
+
now = datetime.now()
|
|
121
|
+
results = model(
|
|
122
|
+
app_name="__temp_token",
|
|
123
|
+
api_token=results,
|
|
124
|
+
generated_by="__temp_token",
|
|
125
|
+
generated_at=now,
|
|
126
|
+
last_access=now,
|
|
127
|
+
permission="r",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return results
|
|
131
|
+
|
|
132
|
+
# [API TOKEN] update api token
|
|
133
|
+
async def update_api_token(
|
|
134
|
+
self,
|
|
135
|
+
workspace_id: str,
|
|
136
|
+
base_name: str,
|
|
137
|
+
app_name: str,
|
|
138
|
+
permission: str = "rw",
|
|
139
|
+
model: BaseModel = ApiToken,
|
|
140
|
+
):
|
|
141
|
+
METHOD = "PUT"
|
|
142
|
+
URL = f"/api/v2.1/workspace/{workspace_id}/dtable/{base_name}/api-tokens/{app_name}"
|
|
143
|
+
JSON = {"permission": permission}
|
|
144
|
+
|
|
145
|
+
async with self.session_maker(token=self.account_token) as session:
|
|
146
|
+
response = await self.request(session=session, method=METHOD, url=URL, json=JSON)
|
|
147
|
+
results = model(**response) if model else response
|
|
148
|
+
|
|
149
|
+
return results
|
|
150
|
+
|
|
151
|
+
# [API TOKEN] delete api token
|
|
152
|
+
async def delete_api_token(self, workspace_id: str, base_name: str, app_name: str):
|
|
153
|
+
METHOD = "DELETE"
|
|
154
|
+
URL = f"/api/v2.1/workspace/{workspace_id}/dtable/{base_name}/api-tokens/{app_name}"
|
|
155
|
+
ITEM = "success"
|
|
156
|
+
|
|
157
|
+
async with self.session_maker(token=self.account_token) as session:
|
|
158
|
+
response = await self.request(session=session, method=METHOD, url=URL)
|
|
159
|
+
results = response[ITEM]
|
|
160
|
+
|
|
161
|
+
return results
|
|
162
|
+
|
|
163
|
+
################################################################
|
|
164
|
+
# AUTHENTICATION - BASE TOKEN
|
|
165
|
+
################################################################
|
|
166
|
+
# [BASE TOKEN] get base token with account token
|
|
167
|
+
async def get_base_token_with_account_token(
|
|
168
|
+
self,
|
|
169
|
+
workspace_id: str = None,
|
|
170
|
+
base_name: str = None,
|
|
171
|
+
model: BaseModel = BaseToken,
|
|
172
|
+
):
|
|
173
|
+
METHOD = "GET"
|
|
174
|
+
URL = f"/api/v2.1/workspace/{workspace_id}/dtable/{base_name}/access-token/"
|
|
175
|
+
|
|
176
|
+
async with self.session_maker(token=self.account_token) as session:
|
|
177
|
+
results = await self.request(session=session, method=METHOD, url=URL)
|
|
178
|
+
if model:
|
|
179
|
+
results = model(**results)
|
|
180
|
+
|
|
181
|
+
return results
|
|
182
|
+
|
|
183
|
+
# [BASE TOKEN] get base token with api token
|
|
184
|
+
async def get_base_token_with_api_token(self, api_token: str, model: BaseModel = BaseToken):
|
|
185
|
+
METHOD = "GET"
|
|
186
|
+
URL = "/api/v2.1/dtable/app-access-token/"
|
|
187
|
+
|
|
188
|
+
async with self.session_maker(token=api_token) as session:
|
|
189
|
+
results = await self.request(session=session, method=METHOD, url=URL)
|
|
190
|
+
if model:
|
|
191
|
+
results = model(**results)
|
|
192
|
+
|
|
193
|
+
return results
|
|
194
|
+
|
|
195
|
+
# [BASE TOKEN] get base token with invite link
|
|
196
|
+
async def get_base_token_with_invite_link(self, link: str, model: BaseModel = BaseToken):
|
|
197
|
+
link = link.rsplit("/links/", 1)[-1].strip("/")
|
|
198
|
+
METHOD = "GET"
|
|
199
|
+
URL = "/api/v2.1/dtable/share-link-access-token/"
|
|
200
|
+
|
|
201
|
+
async with self.session_maker(token=link) as session:
|
|
202
|
+
results = await self.request(session=session, method=METHOD, url=URL)
|
|
203
|
+
if model:
|
|
204
|
+
results = model(**results)
|
|
205
|
+
|
|
206
|
+
return results
|
|
207
|
+
|
|
208
|
+
# [BASE TOKEN] get base token with external link
|
|
209
|
+
async def get_base_token_with_external_link(self, link: str, model: BaseModel = BaseToken):
|
|
210
|
+
link = link.rsplit("/external-links/", 1)[-1].strip("/")
|
|
211
|
+
METHOD = "GET"
|
|
212
|
+
URL = f"/api/v2.1/external-link-tokens/{link}/access-token/"
|
|
213
|
+
|
|
214
|
+
async with self.session_maker(token=link) as session:
|
|
215
|
+
results = await self.request(session=session, method=METHOD, url=URL)
|
|
216
|
+
if model:
|
|
217
|
+
results = model(**results)
|
|
218
|
+
|
|
219
|
+
return results
|
|
220
|
+
|
|
221
|
+
################################################################
|
|
222
|
+
# (CUSTOM) GET BASE CLIENT
|
|
223
|
+
################################################################
|
|
224
|
+
# [BASE CLIENT] (custom) get base client with account token
|
|
225
|
+
async def get_base_client_with_account_token(self, workspace_id: str, base_name: str):
|
|
226
|
+
base_token = await self.get_base_token_with_account_token(workspace_id=workspace_id, base_name=base_name)
|
|
227
|
+
return BaseClient(base_token=base_token)
|