Skip to content

Commit 7e4ca46

Browse files
committed
Merge branch 'dev'
1 parent 8112044 commit 7e4ca46

File tree

8 files changed

+217
-39
lines changed

8 files changed

+217
-39
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
__pycache__
55
testrun.py
66
test_config.json*
7+
build

README.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@
33
Python client for NocoDB API v2
44

55

6+
## Install
7+
8+
Install from [pypi](https://pypi.org/project/nocodb-api/):
9+
10+
```shell
11+
pip install nocodb-api
12+
```
13+
14+
Install from Github:
15+
16+
```shell
17+
pip install "nocodb-api@git+https://github.com/infeeeee/py-nocodb"
18+
```
19+
620
## Quickstart
721

822
```python
@@ -17,6 +31,20 @@ table = base.get_table_by_title("Sample Views")
1731
[print(i, r.metadata) for i,r in enumerate(table.get_records())]
1832
```
1933

34+
Get debug log:
35+
36+
```python
37+
import logging
38+
from nocodb import NocoDB
39+
40+
logging.basicConfig()
41+
logging.getLogger('nocodb').setLevel(logging.DEBUG)
42+
# Now every log is visible.
43+
44+
# Limit to submodules:
45+
logging.getLogger('nocodb.Base').setLevel(logging.DEBUG)
46+
```
47+
2048

2149
## Development
2250

@@ -31,4 +59,19 @@ Create a file `test_config.json` with the parameters, or change the Environment
3159

3260
```shell
3361
docker run --rm -it $(docker build -q -f tests/Dockerfile .)
34-
```
62+
```
63+
64+
### Official docs
65+
66+
- https://meta-apis-v2.nocodb.com
67+
- https://data-apis-v2.nocodb.com
68+
- https://docs.nocodb.com
69+
70+
### Documentation with [pdoc](https://pdoc.dev)
71+
72+
*TODO*
73+
74+
```shell
75+
pip install -e ".[doc]"
76+
pdoc -d google nocodb
77+
```

nocodb/Base.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from nocodb.Table import Table
99

1010
import logging
11-
logger = logging.getLogger(__name__)
12-
logger.addHandler(logging.NullHandler())
11+
_logger = logging.getLogger(__name__)
12+
_logger.addHandler(logging.NullHandler())
1313

1414

1515
class Base:
@@ -33,14 +33,14 @@ def duplicate(self,
3333
"excludeData": exclude_data,
3434
"excludeViews": exclude_views,
3535
"excludeHooks": exclude_hooks})
36-
logger.debug(f"Base {self.title} duplicated")
36+
_logger.info(f"Base {self.title} duplicated")
3737

3838
return self.noco_db.get_base(base_id=r.json()["base_id"])
3939

4040
def delete(self) -> bool:
4141
r = self.noco_db.call_noco(path=f"meta/bases/{self.base_id}",
4242
method="DELETE")
43-
logger.debug(f"Base {self.title} deleted")
43+
_logger.info(f"Base {self.title} deleted")
4444
return r.json()
4545

4646
def update(self, **kwargs) -> None:
@@ -54,14 +54,15 @@ def get_base_info(self) -> dict:
5454

5555
def get_tables(self) -> list[Table]:
5656
r = self.noco_db.call_noco(path=f"meta/bases/{self.base_id}/tables")
57-
tables = [Table(base=self, **t) for t in r.json()["list"]]
58-
logger.debug(f"Tables in base {self.title}: {[t.title for t in tables]}")
57+
tables = [Table(noco_db=self.noco_db, **t) for t in r.json()["list"]]
58+
_logger.debug(f"Tables in base {self.title}: "
59+
+ str([t.title for t in tables]))
5960
return tables
6061

6162
def get_table(self, table_id: str) -> Table:
6263
r = self.noco_db.call_noco(
6364
path=f"meta/tables/{table_id}")
64-
return Table(base=self, **r.json())
65+
return Table(noco_db=self.noco_db, **r.json())
6566

6667
def get_table_by_title(self, title: str) -> Table:
6768
try:

nocodb/Column.py

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,107 @@
11
from __future__ import annotations
2+
3+
4+
from typing import TYPE_CHECKING
5+
if TYPE_CHECKING:
6+
from nocodb.Table import Table
7+
from nocodb import NocoDB
8+
9+
10+
class DataType:
11+
12+
def __init__(self, uidt: str) -> None:
13+
self.name = uidt
14+
15+
def __str__(self) -> str:
16+
return self.name
17+
18+
219
class Column:
3-
def __init__(self, **kwargs) -> None:
20+
def __init__(self, noco_db: "NocoDB",**kwargs) -> None:
21+
self.noco_db = noco_db
422
self.title = kwargs["title"]
523
self.column_id = kwargs["id"]
24+
self.table_id = kwargs["fk_model_id"]
25+
626
self.system = bool(kwargs["system"])
727
self.primary_key = bool(kwargs["pk"])
28+
29+
self.data_type = Column.DataType.get_data_type(kwargs["uidt"])
830
self.metadata = kwargs
931

32+
if "colOptions" in kwargs and "fk_related_model_id" in kwargs["colOptions"]:
33+
self.linked_table_id = kwargs["colOptions"]["fk_related_model_id"]
34+
35+
def get_linked_table(self) -> Table:
36+
if hasattr(self, "linked_table_id"):
37+
return self.noco_db.get_table(self.linked_table_id)
38+
else:
39+
raise Exception("Not linked column!")
40+
1041
@staticmethod
1142
def get_id_metadata() -> list[dict]:
1243
return [
13-
{'title': 'Id', 'column_name': 'id', 'uidt': 'ID',
44+
{'title': 'Id', 'column_name': 'id', 'uidt': str(Column.DataType.ID),
1445
'dt': 'int4', 'np': '11', 'ns': '0', 'clen': None,
1546
'pk': True, 'pv': None, 'rqd': True, 'ct': 'int(11)', 'ai': True,
1647
'dtx': 'integer', 'dtxp': '11', },
17-
{'title': 'Title', 'column_name': 'title', 'uidt': 'SingleLineText',
48+
{'title': 'Title', 'column_name': 'title', 'uidt': str(Column.DataType.SingleLineText),
1849
'dt': 'character varying', 'np': None, 'ns': None, 'clen': '45',
1950
'pk': False, 'pv': True, 'rqd': False, 'ct': 'varchar(45)', 'ai': False,
2051
'dtx': 'specificType', 'dtxp': '45', }
2152
]
53+
54+
class DataType:
55+
Formula = DataType("Formula")
56+
57+
LinkToAnotherRecord = DataType("LinkToAnotherRecord")
58+
Links = DataType("Links")
59+
60+
Lookup = DataType("Lookup")
61+
Rollup = DataType("Rollup")
62+
63+
Attachment = DataType("Attachment")
64+
AutoNumber = DataType("AutoNumber")
65+
Barcode = DataType("Barcode")
66+
Button = DataType("Button")
67+
Checkbox = DataType("Checkbox")
68+
Collaborator = DataType("Collaborator")
69+
Count = DataType("Count")
70+
CreatedBy = DataType("CreatedBy")
71+
CreatedTime = DataType("CreatedTime")
72+
Currency = DataType("Currency")
73+
Date = DataType("Date")
74+
DateTime = DataType("DateTime")
75+
Decimal = DataType("Decimal")
76+
Duration = DataType("Duration")
77+
Email = DataType("Email")
78+
ForeignKey = DataType("ForeignKey")
79+
GeoData = DataType("GeoData")
80+
Geometry = DataType("Geometry")
81+
ID = DataType("ID")
82+
JSON = DataType("JSON")
83+
LastModifiedBy = DataType("LastModifiedBy")
84+
LastModifiedTime = DataType("LastModifiedTime")
85+
LongText = DataType("LongText")
86+
MultiSelect = DataType("MultiSelect")
87+
Number = DataType("Number")
88+
Percent = DataType("Percent")
89+
PhoneNumber = DataType("PhoneNumber")
90+
QrCode = DataType("QrCode")
91+
Rating = DataType("Rating")
92+
SingleLineText = DataType("SingleLineText")
93+
SingleSelect = DataType("SingleSelect")
94+
SpecificDBType = DataType("SpecificDBType")
95+
Time = DataType("Time")
96+
URL = DataType("URL")
97+
User = DataType("User")
98+
Year = DataType("Year")
99+
100+
@classmethod
101+
def get_data_type(cls, uidt: str) -> DataType:
102+
if hasattr(cls, uidt):
103+
return getattr(cls, uidt)
104+
else:
105+
raise Exception(f"Invalid datatype {uidt}")
106+
107+

nocodb/Record.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from __future__ import annotations
2-
from typing import TYPE_CHECKING
2+
from typing import TYPE_CHECKING, Any
33

44
from nocodb.Column import Column
55
if TYPE_CHECKING:
@@ -29,3 +29,40 @@ def link_records(self, column: Column, link_records: list["Record"]) -> bool:
2929
method="POST", json=[{"Id": l.record_id} for l in link_records])
3030

3131
return r.json()
32+
33+
def get_linked_records(self, column: Column) -> list[Record]:
34+
path = (f"tables/{self.table.table_id}/links/" +
35+
f"{column.column_id}/records/{self.record_id}")
36+
r = self.noco_db.call_noco(path=path)
37+
38+
if "list" in r.json():
39+
if not r.json()["list"]:
40+
return []
41+
elif isinstance(r.json()["list"], list):
42+
record_ids = [l["Id"] for l in r.json()["list"]]
43+
elif "Id" in r.json()["list"]:
44+
record_ids = [r.json()["list"]["Id"]]
45+
else:
46+
raise Exception("Invalid response")
47+
else:
48+
record_ids = [r.json()["Id"]]
49+
50+
linked_table = self.noco_db.get_table(column.linked_table_id)
51+
return [linked_table.get_record(i) for i in record_ids]
52+
53+
def get_value(self, field: str) -> Any:
54+
try:
55+
return self.metadata[field]
56+
except KeyError:
57+
raise Exception(f"Value for {field} not found!")
58+
59+
def get_column_value(self, column: Column) -> Any:
60+
return self.get_value(column.title)
61+
62+
def get_attachments(self, field: str, encoding: str = "utf-8") -> list[str]:
63+
value_list = self.get_value(field)
64+
if not isinstance(value_list, list):
65+
raise Exception("Invalid field value")
66+
67+
return [self.noco_db.get_file(p["signedPath"], encoding=encoding)
68+
for p in value_list]

nocodb/Table.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
from __future__ import annotations
22
import re
33
from nocodb.Record import Record
4-
from nocodb.Column import Column
4+
from nocodb.Column import Column, DataType
55

66
from typing import TYPE_CHECKING
77
if TYPE_CHECKING:
88
from nocodb.Base import Base
9+
from nocodb import NocoDB
10+
911

1012
import logging
11-
logger = logging.getLogger(__name__)
12-
logger.addHandler(logging.NullHandler())
13+
_logger = logging.getLogger(__name__)
14+
_logger.addHandler(logging.NullHandler())
1315

1416

1517
class Table:
16-
def __init__(self, base: "Base", **kwargs) -> None:
18+
def __init__(self, noco_db: "NocoDB", **kwargs) -> None:
1719

18-
self.base = base
19-
self.noco_db = base.noco_db
20+
self.noco_db = noco_db
21+
self.base_id = kwargs["base_id"]
2022

2123
self.table_id = kwargs["id"]
2224
self.title = kwargs["title"]
@@ -32,10 +34,10 @@ def get_number_of_records(self) -> int:
3234
path=f"tables/{self.table_id}/records/count")
3335
return r.json()["count"]
3436

35-
def get_columns(self, include_system: bool = True) -> list[Column]:
37+
def get_columns(self, include_system: bool = False) -> list[Column]:
3638
r = self.noco_db.call_noco(
3739
path=f"meta/tables/{self.table_id}")
38-
cols = [Column(**f) for f in r.json()["columns"]]
40+
cols = [Column(noco_db=self.noco_db, **f) for f in r.json()["columns"]]
3941
if include_system:
4042
return cols
4143
else:
@@ -53,11 +55,11 @@ def get_column_by_title(self, title: str) -> Column:
5355
raise Exception(f"Column with title {title} not found!")
5456

5557
def create_column(self, column_name: str,
56-
title: str, uidt: str = "SingleLineText",
58+
title: str, data_type: DataType = Column.DataType.SingleLineText,
5759
**kwargs) -> Column:
5860
kwargs["column_name"] = column_name
5961
kwargs["title"] = title
60-
kwargs["uidt"] = uidt
62+
kwargs["uidt"] = str(data_type)
6163

6264
r = self.noco_db.call_noco(path=f"meta/tables/{self.table_id}/columns",
6365
method="POST",
@@ -67,18 +69,18 @@ def create_column(self, column_name: str,
6769
def duplicate(self,
6870
exclude_data: bool = True,
6971
exclude_views: bool = True) -> None:
70-
r = self.noco_db.call_noco(path=f"meta/duplicate/{self.base.base_id}/table/{self.table_id}",
72+
r = self.noco_db.call_noco(path=f"meta/duplicate/{self.base_id}/table/{self.table_id}",
7173
method="POST",
7274
json={"excludeData": exclude_data,
7375
"excludeViews": exclude_views})
74-
logger.debug(f"Table {self.title} duplicated")
76+
_logger.info(f"Table {self.title} duplicated")
7577
return
7678

7779
# Bug in noco API, wrong Id response
7880

7981
def get_duplicates(self) -> list["Table"]:
8082
duplicates = {}
81-
for t in self.base.get_tables():
83+
for t in self.get_base().get_tables():
8284
if re.match(f"^{self.title} copy(_\\d+)?$", t.title):
8385
nr = re.findall("_(\\d+)", t.title)
8486
if nr:
@@ -91,7 +93,7 @@ def get_duplicates(self) -> list["Table"]:
9193
def delete(self) -> bool:
9294
r = self.noco_db.call_noco(path=f"meta/tables/{self.table_id}",
9395
method="DELETE")
94-
logger.debug(f"Table {self.title} deleted")
96+
_logger.info(f"Table {self.title} deleted")
9597
return r.json()
9698

9799
def get_records(self, params: dict | None = None) -> list[Record]:
@@ -144,3 +146,6 @@ def create_records(self, records: list[dict]) -> list[Record]:
144146
json=records)
145147
ids_string = ','.join([str(d["Id"]) for d in r.json()])
146148
return self.get_records(params={"where": f"(Id,in,{ids_string})"})
149+
150+
def get_base(self) -> Base:
151+
return self.noco_db.get_base(self.base_id)

0 commit comments

Comments
 (0)