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.
Files changed (48) hide show
  1. plantable-0.0.1/PKG-INFO +35 -0
  2. plantable-0.0.1/README.md +57 -0
  3. plantable-0.0.1/pyproject.toml +7 -0
  4. plantable-0.0.1/setup.cfg +65 -0
  5. plantable-0.0.1/src/plantable/__init__.py +9 -0
  6. plantable-0.0.1/src/plantable/client/__init__.py +4 -0
  7. plantable-0.0.1/src/plantable/client/account.py +227 -0
  8. plantable-0.0.1/src/plantable/client/admin.py +817 -0
  9. plantable-0.0.1/src/plantable/client/base/__init__.py +1 -0
  10. plantable-0.0.1/src/plantable/client/base/base.py +885 -0
  11. plantable-0.0.1/src/plantable/client/base/builtin.py +917 -0
  12. plantable-0.0.1/src/plantable/client/conf.py +12 -0
  13. plantable-0.0.1/src/plantable/client/core.py +95 -0
  14. plantable-0.0.1/src/plantable/client/exception.py +2 -0
  15. plantable-0.0.1/src/plantable/client/user.py +818 -0
  16. plantable-0.0.1/src/plantable/const.py +22 -0
  17. plantable-0.0.1/src/plantable/model/__init__.py +4 -0
  18. plantable-0.0.1/src/plantable/model/account.py +92 -0
  19. plantable-0.0.1/src/plantable/model/column.py +324 -0
  20. plantable-0.0.1/src/plantable/model/core.py +391 -0
  21. plantable-0.0.1/src/plantable/model/event.py +56 -0
  22. plantable-0.0.1/src/plantable/model/form.py +91 -0
  23. plantable-0.0.1/src/plantable/scripts.py +46 -0
  24. plantable-0.0.1/src/plantable/serde/__init__.py +2 -0
  25. plantable-0.0.1/src/plantable/serde/deserializer/__init__.py +3 -0
  26. plantable-0.0.1/src/plantable/serde/deserializer/deserializer.py +231 -0
  27. plantable-0.0.1/src/plantable/serde/deserializer/to_avro.py +100 -0
  28. plantable-0.0.1/src/plantable/serde/deserializer/to_postgres.py +341 -0
  29. plantable-0.0.1/src/plantable/serde/deserializer/to_python.py +338 -0
  30. plantable-0.0.1/src/plantable/serde/serializer/__init__.py +1 -0
  31. plantable-0.0.1/src/plantable/serde/serializer/from_arrow.py +177 -0
  32. plantable-0.0.1/src/plantable/serde/serializer/from_python.py +120 -0
  33. plantable-0.0.1/src/plantable/server/__init__.py +0 -0
  34. plantable-0.0.1/src/plantable/server/app.py +51 -0
  35. plantable-0.0.1/src/plantable/server/conf.py +27 -0
  36. plantable-0.0.1/src/plantable/server/router/__init__.py +1 -0
  37. plantable-0.0.1/src/plantable/server/router/api_token.py +57 -0
  38. plantable-0.0.1/src/plantable/server/router/user.py +69 -0
  39. plantable-0.0.1/src/plantable/server/util.py +126 -0
  40. plantable-0.0.1/src/plantable/static/__init__.py +0 -0
  41. plantable-0.0.1/src/plantable/templates.py +76 -0
  42. plantable-0.0.1/src/plantable/utils.py +69 -0
  43. plantable-0.0.1/src/plantable.egg-info/PKG-INFO +35 -0
  44. plantable-0.0.1/src/plantable.egg-info/SOURCES.txt +47 -0
  45. plantable-0.0.1/src/plantable.egg-info/dependency_links.txt +1 -0
  46. plantable-0.0.1/src/plantable.egg-info/entry_points.txt +2 -0
  47. plantable-0.0.1/src/plantable.egg-info/requires.txt +28 -0
  48. plantable-0.0.1/src/plantable.egg-info/top_level.txt +1 -0
@@ -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,7 @@
1
+ [build-system]
2
+ requires = [ "setuptools>=41", "wheel", "setuptools-git-versioning" ]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools-git-versioning]
6
+ enabled = true
7
+ template = "{tag}"
@@ -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,9 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from .client import AccountClient, AdminClient, BaseClient, UserClient
4
+
5
+ try:
6
+ __version__ = version("plantable")
7
+ except PackageNotFoundError:
8
+ # package is not installed
9
+ pass
@@ -0,0 +1,4 @@
1
+ from .account import AccountClient
2
+ from .admin import AdminClient
3
+ from .base import BaseClient
4
+ from .user import UserClient
@@ -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)