Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
infeeeee committed Sep 30, 2024
1 parent 8112044 commit 7e4ca46
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 39 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
__pycache__
testrun.py
test_config.json*
build
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
Python client for NocoDB API v2


## Install

Install from [pypi](https://pypi.org/project/nocodb-api/):

```shell
pip install nocodb-api
```

Install from Github:

```shell
pip install "nocodb-api@git+https://github.com/infeeeee/py-nocodb"
```

## Quickstart

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

Get debug log:

```python
import logging
from nocodb import NocoDB

logging.basicConfig()
logging.getLogger('nocodb').setLevel(logging.DEBUG)
# Now every log is visible.

# Limit to submodules:
logging.getLogger('nocodb.Base').setLevel(logging.DEBUG)
```


## Development

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

```shell
docker run --rm -it $(docker build -q -f tests/Dockerfile .)
```
```

### Official docs

- https://meta-apis-v2.nocodb.com
- https://data-apis-v2.nocodb.com
- https://docs.nocodb.com

### Documentation with [pdoc](https://pdoc.dev)

*TODO*

```shell
pip install -e ".[doc]"
pdoc -d google nocodb
```
15 changes: 8 additions & 7 deletions nocodb/Base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from nocodb.Table import Table

import logging
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
_logger = logging.getLogger(__name__)
_logger.addHandler(logging.NullHandler())


class Base:
Expand All @@ -33,14 +33,14 @@ def duplicate(self,
"excludeData": exclude_data,
"excludeViews": exclude_views,
"excludeHooks": exclude_hooks})
logger.debug(f"Base {self.title} duplicated")
_logger.info(f"Base {self.title} duplicated")

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

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

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

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

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

def get_table_by_title(self, title: str) -> Table:
try:
Expand Down
92 changes: 89 additions & 3 deletions nocodb/Column.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,107 @@
from __future__ import annotations


from typing import TYPE_CHECKING
if TYPE_CHECKING:
from nocodb.Table import Table
from nocodb import NocoDB


class DataType:

def __init__(self, uidt: str) -> None:
self.name = uidt

def __str__(self) -> str:
return self.name


class Column:
def __init__(self, **kwargs) -> None:
def __init__(self, noco_db: "NocoDB",**kwargs) -> None:
self.noco_db = noco_db
self.title = kwargs["title"]
self.column_id = kwargs["id"]
self.table_id = kwargs["fk_model_id"]

self.system = bool(kwargs["system"])
self.primary_key = bool(kwargs["pk"])

self.data_type = Column.DataType.get_data_type(kwargs["uidt"])
self.metadata = kwargs

if "colOptions" in kwargs and "fk_related_model_id" in kwargs["colOptions"]:
self.linked_table_id = kwargs["colOptions"]["fk_related_model_id"]

def get_linked_table(self) -> Table:
if hasattr(self, "linked_table_id"):
return self.noco_db.get_table(self.linked_table_id)
else:
raise Exception("Not linked column!")

@staticmethod
def get_id_metadata() -> list[dict]:
return [
{'title': 'Id', 'column_name': 'id', 'uidt': 'ID',
{'title': 'Id', 'column_name': 'id', 'uidt': str(Column.DataType.ID),
'dt': 'int4', 'np': '11', 'ns': '0', 'clen': None,
'pk': True, 'pv': None, 'rqd': True, 'ct': 'int(11)', 'ai': True,
'dtx': 'integer', 'dtxp': '11', },
{'title': 'Title', 'column_name': 'title', 'uidt': 'SingleLineText',
{'title': 'Title', 'column_name': 'title', 'uidt': str(Column.DataType.SingleLineText),
'dt': 'character varying', 'np': None, 'ns': None, 'clen': '45',
'pk': False, 'pv': True, 'rqd': False, 'ct': 'varchar(45)', 'ai': False,
'dtx': 'specificType', 'dtxp': '45', }
]

class DataType:
Formula = DataType("Formula")

LinkToAnotherRecord = DataType("LinkToAnotherRecord")
Links = DataType("Links")

Lookup = DataType("Lookup")
Rollup = DataType("Rollup")

Attachment = DataType("Attachment")
AutoNumber = DataType("AutoNumber")
Barcode = DataType("Barcode")
Button = DataType("Button")
Checkbox = DataType("Checkbox")
Collaborator = DataType("Collaborator")
Count = DataType("Count")
CreatedBy = DataType("CreatedBy")
CreatedTime = DataType("CreatedTime")
Currency = DataType("Currency")
Date = DataType("Date")
DateTime = DataType("DateTime")
Decimal = DataType("Decimal")
Duration = DataType("Duration")
Email = DataType("Email")
ForeignKey = DataType("ForeignKey")
GeoData = DataType("GeoData")
Geometry = DataType("Geometry")
ID = DataType("ID")
JSON = DataType("JSON")
LastModifiedBy = DataType("LastModifiedBy")
LastModifiedTime = DataType("LastModifiedTime")
LongText = DataType("LongText")
MultiSelect = DataType("MultiSelect")
Number = DataType("Number")
Percent = DataType("Percent")
PhoneNumber = DataType("PhoneNumber")
QrCode = DataType("QrCode")
Rating = DataType("Rating")
SingleLineText = DataType("SingleLineText")
SingleSelect = DataType("SingleSelect")
SpecificDBType = DataType("SpecificDBType")
Time = DataType("Time")
URL = DataType("URL")
User = DataType("User")
Year = DataType("Year")

@classmethod
def get_data_type(cls, uidt: str) -> DataType:
if hasattr(cls, uidt):
return getattr(cls, uidt)
else:
raise Exception(f"Invalid datatype {uidt}")


39 changes: 38 additions & 1 deletion nocodb/Record.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

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

return r.json()

def get_linked_records(self, column: Column) -> list[Record]:
path = (f"tables/{self.table.table_id}/links/" +
f"{column.column_id}/records/{self.record_id}")
r = self.noco_db.call_noco(path=path)

if "list" in r.json():
if not r.json()["list"]:
return []
elif isinstance(r.json()["list"], list):
record_ids = [l["Id"] for l in r.json()["list"]]
elif "Id" in r.json()["list"]:
record_ids = [r.json()["list"]["Id"]]
else:
raise Exception("Invalid response")
else:
record_ids = [r.json()["Id"]]

linked_table = self.noco_db.get_table(column.linked_table_id)
return [linked_table.get_record(i) for i in record_ids]

def get_value(self, field: str) -> Any:
try:
return self.metadata[field]
except KeyError:
raise Exception(f"Value for {field} not found!")

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

def get_attachments(self, field: str, encoding: str = "utf-8") -> list[str]:
value_list = self.get_value(field)
if not isinstance(value_list, list):
raise Exception("Invalid field value")

return [self.noco_db.get_file(p["signedPath"], encoding=encoding)
for p in value_list]
33 changes: 19 additions & 14 deletions nocodb/Table.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
from __future__ import annotations
import re
from nocodb.Record import Record
from nocodb.Column import Column
from nocodb.Column import Column, DataType

from typing import TYPE_CHECKING
if TYPE_CHECKING:
from nocodb.Base import Base
from nocodb import NocoDB


import logging
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
_logger = logging.getLogger(__name__)
_logger.addHandler(logging.NullHandler())


class Table:
def __init__(self, base: "Base", **kwargs) -> None:
def __init__(self, noco_db: "NocoDB", **kwargs) -> None:

self.base = base
self.noco_db = base.noco_db
self.noco_db = noco_db
self.base_id = kwargs["base_id"]

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

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

def create_column(self, column_name: str,
title: str, uidt: str = "SingleLineText",
title: str, data_type: DataType = Column.DataType.SingleLineText,
**kwargs) -> Column:
kwargs["column_name"] = column_name
kwargs["title"] = title
kwargs["uidt"] = uidt
kwargs["uidt"] = str(data_type)

r = self.noco_db.call_noco(path=f"meta/tables/{self.table_id}/columns",
method="POST",
Expand All @@ -67,18 +69,18 @@ def create_column(self, column_name: str,
def duplicate(self,
exclude_data: bool = True,
exclude_views: bool = True) -> None:
r = self.noco_db.call_noco(path=f"meta/duplicate/{self.base.base_id}/table/{self.table_id}",
r = self.noco_db.call_noco(path=f"meta/duplicate/{self.base_id}/table/{self.table_id}",
method="POST",
json={"excludeData": exclude_data,
"excludeViews": exclude_views})
logger.debug(f"Table {self.title} duplicated")
_logger.info(f"Table {self.title} duplicated")
return

# Bug in noco API, wrong Id response

def get_duplicates(self) -> list["Table"]:
duplicates = {}
for t in self.base.get_tables():
for t in self.get_base().get_tables():
if re.match(f"^{self.title} copy(_\\d+)?$", t.title):
nr = re.findall("_(\\d+)", t.title)
if nr:
Expand All @@ -91,7 +93,7 @@ def get_duplicates(self) -> list["Table"]:
def delete(self) -> bool:
r = self.noco_db.call_noco(path=f"meta/tables/{self.table_id}",
method="DELETE")
logger.debug(f"Table {self.title} deleted")
_logger.info(f"Table {self.title} deleted")
return r.json()

def get_records(self, params: dict | None = None) -> list[Record]:
Expand Down Expand Up @@ -144,3 +146,6 @@ def create_records(self, records: list[dict]) -> list[Record]:
json=records)
ids_string = ','.join([str(d["Id"]) for d in r.json()])
return self.get_records(params={"where": f"(Id,in,{ids_string})"})

def get_base(self) -> Base:
return self.noco_db.get_base(self.base_id)
Loading

0 comments on commit 7e4ca46

Please sign in to comment.