Skip to content

Commit c1c49e0

Browse files
authored
Record methods (#8)
1 parent e7713df commit c1c49e0

File tree

5 files changed

+134
-70
lines changed

5 files changed

+134
-70
lines changed

.github/workflows/unittests.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ name: Unit tests
33
on:
44
# push:
55
# branches: ["main"]
6-
pull_request_target:
7-
types: [labeled]
6+
pull_request:
7+
branches: ["main"]
88
workflow_dispatch:
99

1010
permissions:
@@ -13,7 +13,6 @@ permissions:
1313
jobs:
1414
tests:
1515
name: Unit tests on Cloud
16-
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
1716
strategy:
1817
max-parallel: 1
1918
matrix:

nocodb/Column.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def get_id_metadata() -> list[dict]:
5757
"ai": True,
5858
"dtx": "integer",
5959
"dtxp": "11",
60+
"system": True
6061
},
6162
{
6263
"title": "Title",

nocodb/Record.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22
from typing import TYPE_CHECKING, Any
3+
from pathlib import Path
34

45
from nocodb.Column import Column
56

@@ -57,23 +58,53 @@ def get_linked_records(self, column: Column) -> list[Record]:
5758
record_ids = [r.json()["Id"]]
5859

5960
linked_table = self.noco_db.get_table(column.linked_table_id)
60-
return [linked_table.get_record(i) for i in record_ids]
61+
return linked_table.get_records_by_id(record_ids)
6162

6263
def get_value(self, field: str) -> Any:
63-
try:
64-
return self.metadata[field]
65-
except KeyError:
66-
raise Exception(f"Value for {field} not found!")
64+
return self.get_values([field])[field]
6765

6866
def get_column_value(self, column: Column) -> Any:
6967
return self.get_value(column.title)
7068

69+
def get_values(self, fields: list[str] | None = None, include_system: bool = True) -> dict:
70+
if not include_system:
71+
cols = [c.title for c in self.table.get_columns(include_system)]
72+
if fields:
73+
fields = [f for f in fields if f in cols]
74+
else:
75+
fields = cols
76+
77+
field_str = ",".join(fields) if fields else ""
78+
r = self.noco_db.call_noco(
79+
path=f"tables/{self.table.table_id}/records/{self.record_id}",
80+
params={"fields": field_str}
81+
)
82+
return r.json()
83+
7184
def get_attachments(self, field: str, encoding: str = "utf-8") -> list[str]:
7285
value_list = self.get_value(field)
7386
if not isinstance(value_list, list):
7487
raise Exception("Invalid field value")
7588

7689
return [
77-
self.noco_db.get_file(p["signedPath"], encoding=encoding)
90+
self.noco_db.get_file(p["signedUrl"], encoding=encoding)
7891
for p in value_list
7992
]
93+
94+
def update(self, **kwargs) -> Record:
95+
kwargs["Id"] = self.record_id
96+
r = self.noco_db.call_noco(
97+
path=f"tables/{self.table.table_id}/records",
98+
method="PATCH",
99+
json=kwargs,
100+
)
101+
return self.table.get_record(record_id=r.json()["Id"])
102+
103+
def upload_attachment(
104+
self, field: str, filepath: Path, mimetype: str = ""
105+
) -> Record:
106+
value = self.get_value(field=field) or []
107+
value.append(self.noco_db.upload_file(
108+
filepath=filepath, mimetype=mimetype))
109+
110+
return self.update(**{field: value})

nocodb/Table.py

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,53 @@ def get_basic_metadata(self) -> dict:
3232
return {k: v for k, v in m.items() if not k in extra_keys}
3333

3434
def get_number_of_records(self) -> int:
35-
r = self.noco_db.call_noco(path=f"tables/{self.table_id}/records/count")
35+
r = self.noco_db.call_noco(
36+
path=f"tables/{self.table_id}/records/count")
3637
return r.json()["count"]
3738

39+
def get_base(self) -> Base:
40+
return self.noco_db.get_base(self.base_id)
41+
42+
def duplicate(self, exclude_data: bool = True, exclude_views: bool = True) -> None:
43+
r = self.noco_db.call_noco(
44+
path=f"meta/duplicate/{self.base_id}/table/{self.table_id}",
45+
method="POST",
46+
json={"excludeData": exclude_data, "excludeViews": exclude_views},
47+
)
48+
_logger.info(f"Table {self.title} duplicated")
49+
return
50+
51+
# Bug in noco API, wrong Id response
52+
53+
def get_duplicates(self) -> list["Table"]:
54+
duplicates = {}
55+
for t in self.get_base().get_tables():
56+
if re.match(f"^{self.title} copy(_\\d+)?$", t.title):
57+
nr = re.findall("_(\\d+)", t.title)
58+
if nr:
59+
duplicates[int(nr[0])] = t
60+
else:
61+
duplicates[0] = t
62+
63+
return list(dict(sorted(duplicates.items(), reverse=True)).values())
64+
65+
def delete(self) -> bool:
66+
r = self.noco_db.call_noco(
67+
path=f"meta/tables/{self.table_id}", method="DELETE")
68+
_logger.info(f"Table {self.title} deleted")
69+
return r.json()
70+
3871
def get_columns(self, include_system: bool = False) -> list[Column]:
3972
r = self.noco_db.call_noco(path=f"meta/tables/{self.table_id}")
4073
cols = [Column(noco_db=self.noco_db, **f) for f in r.json()["columns"]]
4174
if include_system:
4275
return cols
4376
else:
44-
return [c for c in cols if not c.system and not c.primary_key]
77+
return [c for c in cols if not c.system]
4578

4679
def get_columns_hash(self) -> str:
47-
r = self.noco_db.call_noco(path=f"meta/tables/{self.table_id}/columns/hash")
80+
r = self.noco_db.call_noco(
81+
path=f"meta/tables/{self.table_id}/columns/hash")
4882
return r.json()["hash"]
4983

5084
def get_column_by_title(self, title: str) -> Column:
@@ -69,34 +103,6 @@ def create_column(
69103
)
70104
return self.get_column_by_title(title=title)
71105

72-
def duplicate(self, exclude_data: bool = True, exclude_views: bool = True) -> None:
73-
r = self.noco_db.call_noco(
74-
path=f"meta/duplicate/{self.base_id}/table/{self.table_id}",
75-
method="POST",
76-
json={"excludeData": exclude_data, "excludeViews": exclude_views},
77-
)
78-
_logger.info(f"Table {self.title} duplicated")
79-
return
80-
81-
# Bug in noco API, wrong Id response
82-
83-
def get_duplicates(self) -> list["Table"]:
84-
duplicates = {}
85-
for t in self.get_base().get_tables():
86-
if re.match(f"^{self.title} copy(_\\d+)?$", t.title):
87-
nr = re.findall("_(\\d+)", t.title)
88-
if nr:
89-
duplicates[int(nr[0])] = t
90-
else:
91-
duplicates[0] = t
92-
93-
return list(dict(sorted(duplicates.items(), reverse=True)).values())
94-
95-
def delete(self) -> bool:
96-
r = self.noco_db.call_noco(path=f"meta/tables/{self.table_id}", method="DELETE")
97-
_logger.info(f"Table {self.title} deleted")
98-
return r.json()
99-
100106
def get_records(self, params: dict | None = None) -> list[Record]:
101107
params = params or {}
102108

@@ -128,9 +134,14 @@ def get_records(self, params: dict | None = None) -> list[Record]:
128134
return records
129135

130136
def get_record(self, record_id: int) -> Record:
131-
r = self.noco_db.call_noco(path=f"tables/{self.table_id}/records/{record_id}")
137+
r = self.noco_db.call_noco(
138+
path=f"tables/{self.table_id}/records/{record_id}")
132139
return Record(self, **r.json())
133140

141+
def get_records_by_id(self, record_ids: list[int]) -> list[Record]:
142+
ids_string = ",".join(map(str, record_ids))
143+
return self.get_records(params={"where": f"(Id,in,{ids_string})"})
144+
134145
def get_records_by_field_value(self, field: str, value) -> list[Record]:
135146
return self.get_records(params={"where": f"({field},eq,{value})"})
136147

@@ -144,26 +155,32 @@ def create_records(self, records: list[dict]) -> list[Record]:
144155
r = self.noco_db.call_noco(
145156
path=f"tables/{self.table_id}/records", method="POST", json=records
146157
)
147-
ids_string = ",".join([str(d["Id"]) for d in r.json()])
148-
return self.get_records(params={"where": f"(Id,in,{ids_string})"})
149158

150-
def get_base(self) -> Base:
151-
return self.noco_db.get_base(self.base_id)
159+
return self.get_records_by_id([r_id["Id"] for r_id in r.json()])
152160

153-
def delete_record(self, record_id: int) -> bool:
161+
def delete_record(self, record_id: int) -> int:
154162
r = self.noco_db.call_noco(
155163
path=f"tables/{self.table_id}/records",
156164
method="DELETE",
157165
json={"Id": record_id},
158166
)
167+
return r.json()["Id"]
159168

160-
return r.json()
169+
def delete_records_by_id(self, record_ids: list[int]) -> list[int]:
170+
r = self.noco_db.call_noco(
171+
path=f"tables/{self.table_id}/records",
172+
method="DELETE",
173+
json=[{"Id": r_id} for r_id in record_ids]
174+
)
175+
return [r_id["Id"] for r_id in r.json()]
176+
177+
def delete_records(self, records: list[Record]) -> list[int]:
178+
return self.delete_records_by_id([rec.record_id for rec in records])
161179

162-
def update_record(self, **kwargs) -> None:
180+
def update_records(self, records: list[dict]) -> list[Record]:
163181
r = self.noco_db.call_noco(
164182
path=f"tables/{self.table_id}/records",
165183
method="PATCH",
166-
json=kwargs,
184+
json=records,
167185
)
168-
169-
return r.json()
186+
return self.get_records_by_id([r_id["Id"] for r_id in r.json()])

nocodb/__init__.py

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import annotations
22
import requests
33
from urllib.parse import urlsplit, urljoin
4+
from pathlib import Path
5+
import mimetypes
46

57
from nocodb.Base import Base
68
from nocodb.Column import Column
@@ -34,25 +36,6 @@ def _get_base_url(url) -> str:
3436
url = url[:i]
3537
return urlsplit(url).geturl() + "/"
3638

37-
def get_file(self, path, encoding: str = "utf-8") -> str:
38-
"""Get a file from the noco server
39-
40-
Args:
41-
path (_type_): _description_
42-
encoding (str, optional): Encoding of the response. Defaults to "utf-8".
43-
44-
Returns:
45-
str: _description_
46-
47-
About encoding: https://requests.readthedocs.io/en/latest/user/quickstart/#response-content
48-
"""
49-
headers = {"xc-token": self.api_key}
50-
url = urljoin(self.base_url, path)
51-
r = requests.get(url=url, headers=headers)
52-
r.encoding = encoding
53-
54-
return r.text
55-
5639
def call_noco(self, path: str, method: str = "GET", **kwargs) -> requests.Response:
5740
headers = {"xc-token": self.api_key}
5841
url = urljoin(self.api_url, path)
@@ -70,6 +53,39 @@ def call_noco(self, path: str, method: str = "GET", **kwargs) -> requests.Respon
7053

7154
return r
7255

56+
def get_file(self, path, encoding: str = "utf-8") -> str:
57+
"""Get a file from the noco server
58+
59+
Args:
60+
path (_type_): _description_
61+
encoding (str, optional): Encoding of the response. Defaults to "utf-8".
62+
63+
Returns:
64+
str: _description_
65+
66+
About encoding: https://requests.readthedocs.io/en/latest/user/quickstart/#response-content
67+
"""
68+
69+
r = self.call_noco(path=path)
70+
r.encoding = encoding
71+
72+
return r.text
73+
74+
def upload_file(self, filepath: Path, mimetype: str = "") -> dict:
75+
if not mimetype:
76+
mt = mimetypes.guess_type(filepath, strict=False)
77+
if mt and mt[0]:
78+
mimetype = mt[0]
79+
else:
80+
mimetype = "text/plain"
81+
82+
r = self.call_noco(
83+
path="storage/upload",
84+
method="POST",
85+
files={"file": (filepath.name, open(filepath, "rb"), mimetype)},
86+
)
87+
return r.json()[0]
88+
7389
def get_bases(self) -> list[Base]:
7490
r = self.call_noco(path="meta/bases")
7591
return [Base(noco_db=self, **f) for f in r.json()["list"]]

0 commit comments

Comments
 (0)