From 7a739e137878606be63a11837500e7da19b00488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez=20Saura?= Date: Tue, 20 Sep 2022 23:49:28 +0200 Subject: [PATCH 01/27] docs: Add installation to README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 75e923a..4c3181f 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,12 @@ NocoDB is a great Airtable alternative. This client allows python developers to use NocoDB API in a simple way. +## Installation + +```bash +pip install nocodb +``` + ## Usage ### Client configuration From 984a18c7d5f617a0205f6900c0f06744e9db6baf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez?= Date: Sat, 24 Sep 2022 00:47:55 +0200 Subject: [PATCH 02/27] Update Readme with contributors image --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 4c3181f..7e540f7 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,9 @@ Feel free to add new capabilities by creating a new MR. ## Contributors +![Contributors image](https://contrib.rocks/image?repo=elchicodepython/python-nocodb) + + - Samuel López Saura @elchicodepython - Ilya Sapunov @davert0 From 9e31e6478217f24d391d58c114beeece9d3a6f25 Mon Sep 17 00:00:00 2001 From: Delena Malan Date: Mon, 27 Feb 2023 15:49:00 +0200 Subject: [PATCH 03/27] Raise exception for unsuccessful requests --- nocodb/infra/requests_client.py | 51 ++++++++++++++++----------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/nocodb/infra/requests_client.py b/nocodb/infra/requests_client.py index fd223ed..2a18afa 100644 --- a/nocodb/infra/requests_client.py +++ b/nocodb/infra/requests_client.py @@ -20,6 +20,11 @@ def __init__(self, auth_token: AuthToken, base_uri: str): self.__session.headers.update({"Content-Type": "application/json"}) self.__api_info = NocoDBAPI(base_uri) + def _request(self, method, url, *args, **kwargs): + response = self.__session(method, url, *args, **kwargs) + response.raise_for_status() + return response + def table_row_list( self, project: NocoDBProject, @@ -27,39 +32,35 @@ def table_row_list( filter_obj: Optional[WhereFilter] = None, params: Optional[dict] = None, ) -> dict: - - response = self.__session.get( + return self._request( + "GET", self.__api_info.get_table_uri(project, table), params=get_query_params(filter_obj, params), - ) - return response.json() + ).json() - def table_row_create( - self, project: NocoDBProject, table: str, body: dict - ) -> dict: - return self.__session.post( - self.__api_info.get_table_uri(project, table), json=body + def table_row_create(self, project: NocoDBProject, table: str, body: dict) -> dict: + return self._request( + "POST", self.__api_info.get_table_uri(project, table), json=body ).json() - def table_row_detail( - self, project: NocoDBProject, table: str, row_id: int - ) -> dict: - return self.__session.get( + def table_row_detail(self, project: NocoDBProject, table: str, row_id: int) -> dict: + return self._request( + "GET", self.__api_info.get_row_detail_uri(project, table, row_id), ).json() def table_row_update( self, project: NocoDBProject, table: str, row_id: int, body: dict ) -> dict: - return self.__session.patch( + return self._request( + "PATCH", self.__api_info.get_row_detail_uri(project, table, row_id), json=body, ).json() - def table_row_delete( - self, project: NocoDBProject, table: str, row_id: int - ) -> int: - return self.__session.delete( + def table_row_delete(self, project: NocoDBProject, table: str, row_id: int) -> int: + return self._request( + "DELETE", self.__api_info.get_row_detail_uri(project, table, row_id), ).json() @@ -71,16 +72,14 @@ def table_row_nested_relations_list( row_id: int, column_name: str, ) -> dict: - return self.__session.get( + return self._request( + "GET", self.__api_info.get_nested_relations_rows_list_uri( project, table, relation_type, row_id, column_name - ) + ), ).json() - def project_create( - self, - body - ): - return self.__session.post( - self.__api_info.get_project_uri(), json=body + def project_create(self, body): + return self._request( + "POST", self.__api_info.get_project_uri(), json=body ).json() From a9c01a19013db43505166d875aa4b933b6720c22 Mon Sep 17 00:00:00 2001 From: Delena Malan Date: Tue, 28 Feb 2023 09:17:07 +0200 Subject: [PATCH 04/27] Add request call --- nocodb/infra/requests_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nocodb/infra/requests_client.py b/nocodb/infra/requests_client.py index 2a18afa..c02d35e 100644 --- a/nocodb/infra/requests_client.py +++ b/nocodb/infra/requests_client.py @@ -21,7 +21,7 @@ def __init__(self, auth_token: AuthToken, base_uri: str): self.__api_info = NocoDBAPI(base_uri) def _request(self, method, url, *args, **kwargs): - response = self.__session(method, url, *args, **kwargs) + response = self.__session.request(method, url, *args, **kwargs) response.raise_for_status() return response From b948dd64bd7413ba7790b6804f7a64992a7ccc4f Mon Sep 17 00:00:00 2001 From: Delena Malan Date: Fri, 3 Mar 2023 10:42:46 +0200 Subject: [PATCH 05/27] Add custom exception --- nocodb/exceptions.py | 6 ++++++ nocodb/infra/requests_client.py | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 nocodb/exceptions.py diff --git a/nocodb/exceptions.py b/nocodb/exceptions.py new file mode 100644 index 0000000..3e317c1 --- /dev/null +++ b/nocodb/exceptions.py @@ -0,0 +1,6 @@ +class NocoDBAPIError(Exception): + def __init__(self, message, status_code, response_json=None, response_text=None): + super().__init__(message) + self.status_code = status_code + self.response_json = response_json + self.response_text = response_text \ No newline at end of file diff --git a/nocodb/infra/requests_client.py b/nocodb/infra/requests_client.py index c02d35e..96a7e48 100644 --- a/nocodb/infra/requests_client.py +++ b/nocodb/infra/requests_client.py @@ -7,6 +7,7 @@ ) from ..api import NocoDBAPI from ..utils import get_query_params +from ..exceptions import NocoDBAPIError import requests @@ -22,7 +23,21 @@ def __init__(self, auth_token: AuthToken, base_uri: str): def _request(self, method, url, *args, **kwargs): response = self.__session.request(method, url, *args, **kwargs) - response.raise_for_status() + try: + response.raise_for_status() + except requests.exceptions.HTTPError as http_error: + response_json = None + try: + response_json = response.json() + except requests.exceptions.JSONDecodeError: + ... + raise NocoDBAPIError( + message=str(http_error), + status_code=http_error.response.status_code, + response_json=response_json, + response_text=response.text + ) + return response def table_row_list( From a4b5f2df945c0dab5a51789bf6de2e9f474a683e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez=20Saura?= Date: Fri, 3 Mar 2023 20:30:45 +0100 Subject: [PATCH 06/27] Coesmetic changes to avoid nesting exceptions --- nocodb/infra/requests_client.py | 11 +++++------ nocodb/infra/requests_client_test.py | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 nocodb/infra/requests_client_test.py diff --git a/nocodb/infra/requests_client.py b/nocodb/infra/requests_client.py index 96a7e48..d04f75c 100644 --- a/nocodb/infra/requests_client.py +++ b/nocodb/infra/requests_client.py @@ -21,16 +21,15 @@ def __init__(self, auth_token: AuthToken, base_uri: str): self.__session.headers.update({"Content-Type": "application/json"}) self.__api_info = NocoDBAPI(base_uri) - def _request(self, method, url, *args, **kwargs): + def _request(self, method: str, url: str, *args, **kwargs): response = self.__session.request(method, url, *args, **kwargs) + response_json = None try: response.raise_for_status() + response_json = response.json() + except requests.exceptions.JSONDecodeError: + ... except requests.exceptions.HTTPError as http_error: - response_json = None - try: - response_json = response.json() - except requests.exceptions.JSONDecodeError: - ... raise NocoDBAPIError( message=str(http_error), status_code=http_error.response.status_code, diff --git a/nocodb/infra/requests_client_test.py b/nocodb/infra/requests_client_test.py new file mode 100644 index 0000000..c394d0c --- /dev/null +++ b/nocodb/infra/requests_client_test.py @@ -0,0 +1,22 @@ +from unittest import mock + +import pytest +import requests + +from .requests_client import NocoDBRequestsClient, requests as requests_lib +from ..exceptions import NocoDBAPIError + + +@mock.patch.object(requests_lib, "Session") +def test_NocoDBAPIError_raised_on_bad_response(mock_requests_session): + mock_session = mock.Mock() + mock_resp = requests.models.Response() + mock_resp.status_code = 401 + mock_requests_session.return_value = mock_session + mock_session.request.return_value = mock_resp + + client = NocoDBRequestsClient(mock.Mock(), "") + with pytest.raises(NocoDBAPIError) as exc_info: + client._request("GET", "/") + + assert exc_info.value.status_code == 401 From 194c9d2a1faf7bc0f73e3080a661c8dd848785e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez=20Saura?= Date: Fri, 3 Mar 2023 20:31:28 +0100 Subject: [PATCH 07/27] Add EOF --- nocodb/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nocodb/exceptions.py b/nocodb/exceptions.py index 3e317c1..bdd39f5 100644 --- a/nocodb/exceptions.py +++ b/nocodb/exceptions.py @@ -3,4 +3,4 @@ def __init__(self, message, status_code, response_json=None, response_text=None) super().__init__(message) self.status_code = status_code self.response_json = response_json - self.response_text = response_text \ No newline at end of file + self.response_text = response_text From d970c03c58931a4f9c4ccba4a1d2bcb9f943ef26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez=20Saura?= Date: Fri, 3 Mar 2023 20:54:47 +0100 Subject: [PATCH 08/27] Add typing to exceptions file and format it --- nocodb/exceptions.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/nocodb/exceptions.py b/nocodb/exceptions.py index bdd39f5..48bdd3a 100644 --- a/nocodb/exceptions.py +++ b/nocodb/exceptions.py @@ -1,5 +1,14 @@ +from typing import Optional + + class NocoDBAPIError(Exception): - def __init__(self, message, status_code, response_json=None, response_text=None): + def __init__( + self, + message: str, + status_code: int, + response_json: Optional[dict] = None, + response_text: Optional[str] = None, + ): super().__init__(message) self.status_code = status_code self.response_json = response_json From b57f6c8b8956c7138373408af8d07aa3ecbefb3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez=20Saura?= Date: Fri, 3 Mar 2023 21:09:01 +0100 Subject: [PATCH 09/27] Update package version to 1.0.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 750c490..fc3574e 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='nocodb', - version='0.1.0', + version='1.0.1', author='Samuel López Saura', author_email='samuellopezsaura@gmail.com', packages=find_packages(), From ce4535fd95565e19069250cff69d93b11d1a1589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez?= Date: Fri, 3 Mar 2023 21:19:13 +0100 Subject: [PATCH 10/27] Add Delena Malan to contributors section of README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7e540f7..1be9f8e 100644 --- a/README.md +++ b/README.md @@ -139,4 +139,5 @@ Feel free to add new capabilities by creating a new MR. - Samuel López Saura @elchicodepython - Ilya Sapunov @davert0 +- Delena Malan @delenamalan From 3dd40a3fcff91af8cfd69d04f4e1eab931e77d0c Mon Sep 17 00:00:00 2001 From: Delena Malan Date: Fri, 3 Mar 2023 22:39:03 +0200 Subject: [PATCH 11/27] Add count and find-one methods (#6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Raise exception for unsuccessful requests * Add request call * Add count * Add find one * Add examples to README.md * Fix README for count Co-authored-by: Samuel López --------- Co-authored-by: Samuel López --- README.md | 6 ++++++ nocodb/api.py | 16 ++++++++++++++++ nocodb/infra/requests_client.py | 25 +++++++++++++++++++++++++ nocodb/utils.py | 2 +- 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1be9f8e..1502d30 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,12 @@ table_rows = client.table_row_list(project, table_name, params={'offset': 100}) table_rows = client.table_row_list(project, table_name, InFilter("name", "sam")) table_rows = client.table_row_list(project, table_name, filter_obj=EqFilter("Id", 100)) +# Filter and count rows +count = client.table_count(project, table_name, filter_obj=EqFilter("Id", 100)) + +# Find one row +table_row = client.table_find_one(project, table_name, filter_obj=EqFilter("Id", 100), params={"sort": "-created_at"}) + # Retrieve a single row row_id = 10 row = client.table_row_detail(project, table_name, row_id) diff --git a/nocodb/api.py b/nocodb/api.py index aed549a..e8b0f17 100644 --- a/nocodb/api.py +++ b/nocodb/api.py @@ -26,6 +26,22 @@ def get_table_uri(self, project: NocoDBProject, table: str) -> str: ) ) + def get_table_count_uri(self, project: NocoDBProject, table: str) -> str: + return "/".join( + ( + self.get_table_uri(project, table), + 'count' + ) + ) + + def get_table_find_one_uri(self, project: NocoDBProject, table: str) -> str: + return "/".join( + ( + self.get_table_uri(project, table), + 'find-one' + ) + ) + def get_row_detail_uri( self, project: NocoDBProject, table: str, row_id: int ): diff --git a/nocodb/infra/requests_client.py b/nocodb/infra/requests_client.py index d04f75c..dfeac08 100644 --- a/nocodb/infra/requests_client.py +++ b/nocodb/infra/requests_client.py @@ -78,6 +78,31 @@ def table_row_delete(self, project: NocoDBProject, table: str, row_id: int) -> i self.__api_info.get_row_detail_uri(project, table, row_id), ).json() + def table_count( + self, + project: NocoDBProject, + table: str, + filter_obj: Optional[WhereFilter] = None, + ) -> dict: + return self._request( + "GET", + self.__api_info.get_table_count_uri(project, table), + params=get_query_params(filter_obj), + ).json() + + def table_find_one( + self, + project: NocoDBProject, + table: str, + filter_obj: Optional[WhereFilter] = None, + params: Optional[dict] = None, + ) -> dict: + return self._request( + "GET", + self.__api_info.get_table_find_one_uri(project, table), + params=get_query_params(filter_obj, params), + ).json() + def table_row_nested_relations_list( self, project: NocoDBProject, diff --git a/nocodb/utils.py b/nocodb/utils.py index 4b37cdf..2b98823 100644 --- a/nocodb/utils.py +++ b/nocodb/utils.py @@ -1,4 +1,4 @@ -def get_query_params(filter_obj, params) -> dict: +def get_query_params(filter_obj, params=None) -> dict: query_params = params or {} if filter_obj: query_params["where"] = filter_obj.get_where() From 254998b02afe343fbccca89d352f565a25c12e0e Mon Sep 17 00:00:00 2001 From: Jan Scheiper Date: Sat, 4 Mar 2023 13:52:35 +0100 Subject: [PATCH 12/27] Add support for table management and column management (#7) * use urljoin instead of string concatenation, added gitignore * added table management endpoints * added table column endpoints * fixed a name clash with the get_table_uri method --- .gitignore | 1 + nocodb/api.py | 70 ++++++++++++++++++++--------- nocodb/infra/requests_client.py | 78 +++++++++++++++++++++++++++++++++ nocodb/nocodb.py | 62 ++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 21 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/nocodb/api.py b/nocodb/api.py index e8b0f17..cacf3a5 100644 --- a/nocodb/api.py +++ b/nocodb/api.py @@ -1,30 +1,26 @@ from enum import Enum +from urllib.parse import urljoin from .nocodb import NocoDBProject class NocoDBAPIUris(Enum): - V1_DB_DATA_PREFIX = "api/v1/db/data" - V1_DB_META_PREFIX = "api/v1/db/meta" + V1_DB_DATA_PREFIX = "api/v1/db/data/" + V1_DB_META_PREFIX = "api/v1/db/meta/" class NocoDBAPI: def __init__(self, base_uri: str): - self.__base_data_uri = ( - f"{base_uri}/{NocoDBAPIUris.V1_DB_DATA_PREFIX.value}" - ) - self.__base_meta_uri = ( - f"{base_uri}/{NocoDBAPIUris.V1_DB_META_PREFIX.value}" - ) + self.__base_data_uri = urljoin(base_uri + "/", NocoDBAPIUris.V1_DB_DATA_PREFIX.value) + self.__base_meta_uri = urljoin(base_uri + "/", NocoDBAPIUris.V1_DB_META_PREFIX.value) def get_table_uri(self, project: NocoDBProject, table: str) -> str: - return "/".join( + return urljoin(self.__base_data_uri, "/".join( ( - self.__base_data_uri, project.org_name, project.project_name, table, ) - ) + )) def get_table_count_uri(self, project: NocoDBProject, table: str) -> str: return "/".join( @@ -45,15 +41,14 @@ def get_table_find_one_uri(self, project: NocoDBProject, table: str) -> str: def get_row_detail_uri( self, project: NocoDBProject, table: str, row_id: int ): - return "/".join( + return urljoin(self.__base_data_uri, "/".join( ( - self.__base_data_uri, project.org_name, project.project_name, table, str(row_id), ) - ) + )) def get_nested_relations_rows_list_uri( self, @@ -63,9 +58,8 @@ def get_nested_relations_rows_list_uri( row_id: int, column_name: str, ) -> str: - return "/".join( + return urljoin(self.__base_data_uri, "/".join( ( - self.__base_data_uri, project.org_name, project.project_name, table, @@ -73,14 +67,48 @@ def get_nested_relations_rows_list_uri( relation_type, column_name, ) - ) + )) def get_project_uri( self, ) -> str: - return "/".join( + return urljoin(self.__base_meta_uri, "projects") + + def get_project_tables_uri( + self, project: NocoDBProject, + ) -> str: + return urljoin(self.__base_meta_uri, "/".join( ( - self.__base_meta_uri, - "projects" + "projects", + project.project_name, + "tables" ) - ) \ No newline at end of file + )) + + def get_table_meta_uri( + self, tableId: str, operation: str = None, + ) -> str: + additional_path = [] + if operation is not None: + additional_path.append(operation) + + return urljoin(self.__base_meta_uri, "/".join( + [ + "tables", + tableId, + ] + additional_path + )) + + def get_column_uri( + self, columnId: str, operation: str = None, + ) -> str: + additional_path = [] + if operation is not None: + additional_path.append(operation) + + return urljoin(self.__base_meta_uri, "/".join( + [ + "columns", + columnId, + ] + additional_path + )) \ No newline at end of file diff --git a/nocodb/infra/requests_client.py b/nocodb/infra/requests_client.py index dfeac08..b191b4c 100644 --- a/nocodb/infra/requests_client.py +++ b/nocodb/infra/requests_client.py @@ -122,3 +122,81 @@ def project_create(self, body): return self._request( "POST", self.__api_info.get_project_uri(), json=body ).json() + + def table_create( + self, project: NocoDBProject, body: dict + ) -> dict: + return self.__session.post( + url=self.__api_info.get_project_tables_uri(project), + json=body, + ).json() + + def table_list( + self, + project: NocoDBProject, + params: Optional[dict] = None, + ) -> dict: + return self.__session.get( + url=self.__api_info.get_project_tables_uri(project), + params=params, + ).json() + + def table_read( + self, tableId: str, + ) -> dict: + return self.__session.get( + url=self.__api_info.get_table_meta_uri(tableId) + ).json() + + def table_update( + self, tableId: str, body: dict + ): + return self.__session.patch( + url=self.__api_info.get_table_meta_uri(tableId), + json=body, + ).json() + + def table_delete( + self, tableId: str, + ) -> dict: + return self.__session.delete( + url=self.__api_info.get_table_meta_uri(tableId) + ).json() + + def table_reorder( + self, tableId: str, order: int + ) -> dict: + return self.__session.post( + url=self.__api_info.get_table_meta_uri(tableId, "reorder"), + json={ "order": order } + ).json() + + def table_column_create( + self, tableId: str, body: dict, + ) -> dict: + return self.__session.post( + url=self.__api_info.get_table_meta_uri(tableId, "columns"), + json=body, + ).json() + + def table_column_update( + self, columnId: str, body: dict, + ) -> dict: + return self.__session.patch( + url=self.__api_info.get_column_uri(columnId), + json=body, + ).json() + + def table_column_delete( + self, columnId: str, + ) -> dict: + return self.__session.delete( + url=self.__api_info.get_column_uri(columnId) + ).json() + + def table_column_set_primary( + self, columnId: str, + ) -> bool: + return self.__session.post( + url=self.__api_info.get_column_uri(columnId, "primary"), + ).json() \ No newline at end of file diff --git a/nocodb/nocodb.py b/nocodb/nocodb.py index 8575a8c..ede80b2 100644 --- a/nocodb/nocodb.py +++ b/nocodb/nocodb.py @@ -134,3 +134,65 @@ def table_row_nested_relations_list( column_name: str, ) -> dict: pass + + @abstractmethod + def table_create( + self, project: NocoDBProject, body: dict + ) -> dict: + pass + + @abstractmethod + def table_list( + self, + project: NocoDBProject, + params: Optional[dict] = None, + ) -> dict: + pass + + @abstractmethod + def table_read( + self, tableId: str, + ) -> dict: + pass + + @abstractmethod + def table_update( + self, tableId: str, body: dict, + ) -> bool: + pass + + @abstractmethod + def table_delete( + self, tableId: str, + ) -> dict: + pass + + @abstractmethod + def table_reorder( + self, tableId: str, order: int, + ) -> dict: + pass + + @abstractmethod + def table_column_create( + self, tableId: str, body: dict, + ) -> dict: + pass + + @abstractmethod + def table_column_update( + self, columnId: str, body: dict, + ) -> dict: + pass + + @abstractmethod + def table_column_delete( + self, columnId: str, + ) -> dict: + pass + + @abstractmethod + def table_column_set_primary( + self, columnId: str, + ) -> dict: + pass \ No newline at end of file From f69b4b00aa67d971538f77eb0eb32a2e7cff06ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez=20Saura?= Date: Sat, 4 Mar 2023 13:55:15 +0100 Subject: [PATCH 13/27] Add missing newlines at the end of files --- .gitignore | 2 +- nocodb/api.py | 2 +- nocodb/infra/requests_client.py | 2 +- nocodb/nocodb.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index ed8ebf5..bee8a64 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -__pycache__ \ No newline at end of file +__pycache__ diff --git a/nocodb/api.py b/nocodb/api.py index cacf3a5..296fa21 100644 --- a/nocodb/api.py +++ b/nocodb/api.py @@ -111,4 +111,4 @@ def get_column_uri( "columns", columnId, ] + additional_path - )) \ No newline at end of file + )) diff --git a/nocodb/infra/requests_client.py b/nocodb/infra/requests_client.py index b191b4c..ebb9fcc 100644 --- a/nocodb/infra/requests_client.py +++ b/nocodb/infra/requests_client.py @@ -199,4 +199,4 @@ def table_column_set_primary( ) -> bool: return self.__session.post( url=self.__api_info.get_column_uri(columnId, "primary"), - ).json() \ No newline at end of file + ).json() diff --git a/nocodb/nocodb.py b/nocodb/nocodb.py index ede80b2..26c3077 100644 --- a/nocodb/nocodb.py +++ b/nocodb/nocodb.py @@ -195,4 +195,4 @@ def table_column_delete( def table_column_set_primary( self, columnId: str, ) -> dict: - pass \ No newline at end of file + pass From 9def9f32d2aa32436d702c1a32542bd9875ad7be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez=20Saura?= Date: Sat, 4 Mar 2023 14:35:28 +0100 Subject: [PATCH 14/27] Update requests_client to use self._request in all the methods --- nocodb/infra/requests_client.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/nocodb/infra/requests_client.py b/nocodb/infra/requests_client.py index ebb9fcc..98e63b5 100644 --- a/nocodb/infra/requests_client.py +++ b/nocodb/infra/requests_client.py @@ -126,7 +126,8 @@ def project_create(self, body): def table_create( self, project: NocoDBProject, body: dict ) -> dict: - return self.__session.post( + return self._request( + "POST", url=self.__api_info.get_project_tables_uri(project), json=body, ).json() @@ -136,7 +137,8 @@ def table_list( project: NocoDBProject, params: Optional[dict] = None, ) -> dict: - return self.__session.get( + return self._request( + "GET", url=self.__api_info.get_project_tables_uri(project), params=params, ).json() @@ -144,14 +146,16 @@ def table_list( def table_read( self, tableId: str, ) -> dict: - return self.__session.get( + return self._request( + "GET", url=self.__api_info.get_table_meta_uri(tableId) ).json() def table_update( self, tableId: str, body: dict ): - return self.__session.patch( + return self._request( + "PATCH", url=self.__api_info.get_table_meta_uri(tableId), json=body, ).json() @@ -159,14 +163,16 @@ def table_update( def table_delete( self, tableId: str, ) -> dict: - return self.__session.delete( + return self._request( + "DELETE", url=self.__api_info.get_table_meta_uri(tableId) ).json() def table_reorder( self, tableId: str, order: int ) -> dict: - return self.__session.post( + return self._request( + "POST", url=self.__api_info.get_table_meta_uri(tableId, "reorder"), json={ "order": order } ).json() @@ -174,7 +180,8 @@ def table_reorder( def table_column_create( self, tableId: str, body: dict, ) -> dict: - return self.__session.post( + return self._request( + "POST", url=self.__api_info.get_table_meta_uri(tableId, "columns"), json=body, ).json() @@ -182,7 +189,8 @@ def table_column_create( def table_column_update( self, columnId: str, body: dict, ) -> dict: - return self.__session.patch( + return self._request( + "PATCH", url=self.__api_info.get_column_uri(columnId), json=body, ).json() @@ -190,13 +198,15 @@ def table_column_update( def table_column_delete( self, columnId: str, ) -> dict: - return self.__session.delete( + return self._request( + "DELETE", url=self.__api_info.get_column_uri(columnId) ).json() def table_column_set_primary( self, columnId: str, ) -> bool: - return self.__session.post( + return self._request( + "POST", url=self.__api_info.get_column_uri(columnId, "primary"), ).json() From d64b2a81bd442dd982951618dfac2cef2c736002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez?= Date: Sat, 4 Mar 2023 14:43:48 +0100 Subject: [PATCH 15/27] Add @jangxx to contributors --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1502d30..879558d 100644 --- a/README.md +++ b/README.md @@ -146,4 +146,5 @@ Feel free to add new capabilities by creating a new MR. - Samuel López Saura @elchicodepython - Ilya Sapunov @davert0 - Delena Malan @delenamalan +- Jan Scheiper @jangxx From e1c0be4fe6914825e96d7230b7e56e1f8aae2d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez=20Saura?= Date: Sat, 4 Mar 2023 00:30:41 +0100 Subject: [PATCH 16/27] [BREAKING CHANGE]: Add new filter types and rename InFilter to LikeFilter --- README.md | 65 +++++++++++++++++++++++++++++++--- nocodb/__init__.py | 2 +- nocodb/filters.py | 19 ---------- nocodb/filters/__init__.py | 12 +++++++ nocodb/filters/factory.py | 15 ++++++++ nocodb/filters/factory_test.py | 15 ++++++++ nocodb/filters/filters_test.py | 20 +++++++++++ nocodb/filters/raw_filter.py | 19 ++++++++++ 8 files changed, 143 insertions(+), 24 deletions(-) delete mode 100644 nocodb/filters.py create mode 100644 nocodb/filters/__init__.py create mode 100644 nocodb/filters/factory.py create mode 100644 nocodb/filters/factory_test.py create mode 100644 nocodb/filters/filters_test.py create mode 100644 nocodb/filters/raw_filter.py diff --git a/README.md b/README.md index 879558d..fbf4439 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ pip install nocodb ### Client configuration ```python from nocodb.nocodb import NocoDBProject, APIToken, JWTAuthToken -from nocodb.filters import InFilter, EqFilter +from nocodb.filters import LikeFilter, EqFilter from nocodb.infra.requests_client import NocoDBRequestsClient @@ -98,7 +98,7 @@ table_rows = client.table_row_list(project, table_name, params={'offset': 100}) # Filter the query # Currently only one filter at a time is allowed. I don't know how to join # multiple conditions in nocodb dsl. If you know how please let me know :). -table_rows = client.table_row_list(project, table_name, InFilter("name", "sam")) +table_rows = client.table_row_list(project, table_name, LikeFilter("name", "%sam%")) table_rows = client.table_row_list(project, table_name, filter_obj=EqFilter("Id", 100)) # Filter and count rows @@ -114,7 +114,7 @@ row = client.table_row_detail(project, table_name, row_id) # Create a new row row_info = { "name": "my thoughts", - "content": "i'm going to buy samuel a beer because i love this module", + "content": "i'm going to buy samuel a beer 🍻 because I 💚 this module", "mood": ":)" } client.table_row_create(project, table_name, row_info) @@ -122,7 +122,7 @@ client.table_row_create(project, table_name, row_info) # Update a row row_id = 2 row_info = { - "content": "i'm going to buy samuel a new car because i love this module", + "content": "i'm going to buy samuel a new car 🚙 because I 💚 this module", } client.table_row_update(project, table_name, row_id, row_info) @@ -130,6 +130,63 @@ client.table_row_update(project, table_name, row_id, row_info) client.table_row_delete(project, table_name, row_id) ``` +### Available filters + +- EqFilter +- EqualFilter (Alias of EqFilter) +- NotEqualFilter +- GreaterThanFilter +- GreaterOrEqualFilter +- LessThanFilter +- LessOrEqualFilter +- LikeFilter + +### Using custom filters + +Nocodb is evolving and new operators are comming with each release. + +Most of the basic operations are inside this package but you could need some new +feature that could not be added yet. +For those filters you can build your own. + +Example for basic filters: + +```python +from nocodb.filters.factory import basic_filter_class_factory + +BasicFilter = basic_filter_class_factory('=') +table_rows = client.table_row_list(project, table_name, BasicFilter('age', '16')) + +``` + +You can find the updated list of all the available nocodb operators [here](https://docs.nocodb.com/developer-resources/rest-apis/#comparison-operators). + +In some cases you might want to write your own filter string as described in the previous link. +For that cases you can use the less-semmantic RawFilter. + +```python +from nocodb.filters.raw_filter import RawFilter + +table_rows = client.table_row_list(project, table_name, RawFilter('(birthday,eq,exactDate,2023-06-01)')) +``` + +In some cases we might want to have a file with some custom raw filters already defined by us. +We can easily create custom raw filter classes using `raw_template_filter_class_factory`. + +```python +from nocodb.filters.factory import raw_template_filter_class_factory + +BirthdayDateFilter = raw_template_filter_class_factory('(birthday,eq,exactDate,{})') +ExactDateEqFilter = raw_template_filter_class_factory('({},eq,exactDate,{})') +ExactDateOpFilter = raw_template_filter_class_factory('({},{op},exactDate,{})') + +table_rows = client.table_row_list(project, table_name, BirthdayDateFilter('2023-06-01')) +table_rows = client.table_row_list(project, table_name, ExactDateEqFilter('column', '2023-06-01')) +table_rows = client.table_row_list(project, table_name, ExactDateOpFilter('column', '2023-06-01', op='eq')) +``` + +Credits to @MitPitt for asking this feature. + ## Author notes I created this package to bootstrap some personal projects and I hope it diff --git a/nocodb/__init__.py b/nocodb/__init__.py index f102a9c..8c0d5d5 100644 --- a/nocodb/__init__.py +++ b/nocodb/__init__.py @@ -1 +1 @@ -__version__ = "0.0.1" +__version__ = "2.0.0" diff --git a/nocodb/filters.py b/nocodb/filters.py deleted file mode 100644 index 6fa7b4b..0000000 --- a/nocodb/filters.py +++ /dev/null @@ -1,19 +0,0 @@ -from .nocodb import WhereFilter - - -class InFilter(WhereFilter): - def __init__(self, column_name: str, value: str): - self.__column_name = column_name - self.__value = value - - def get_where(self) -> str: - return f"({self.__column_name},like,%{self.__value}%)" - - -class EqFilter(WhereFilter): - def __init__(self, column_name: str, value: str): - self.__column_name = column_name - self.__value = value - - def get_where(self) -> str: - return f"({self.__column_name},eq,{self.__value})" diff --git a/nocodb/filters/__init__.py b/nocodb/filters/__init__.py new file mode 100644 index 0000000..fe57678 --- /dev/null +++ b/nocodb/filters/__init__.py @@ -0,0 +1,12 @@ +from ..nocodb import WhereFilter +from .factory import basic_filter_class_factory + + +EqFilter = basic_filter_class_factory('eq') +EqualFilter = EqFilter +NotEqualFilter = basic_filter_class_factory('neq') +GreaterThanFilter = basic_filter_class_factory('gt') +GreaterOrEqualFilter = basic_filter_class_factory('ge') +LessThanFilter = basic_filter_class_factory('lt') +LessOrEqualFilter = basic_filter_class_factory('le') +LikeFilter = basic_filter_class_factory('like') diff --git a/nocodb/filters/factory.py b/nocodb/filters/factory.py new file mode 100644 index 0000000..e1bb8aa --- /dev/null +++ b/nocodb/filters/factory.py @@ -0,0 +1,15 @@ +from ..nocodb import WhereFilter + +from .raw_filter import RawTemplateFilter + + +def basic_filter_class_factory(filter_name: str): + return raw_template_filter_class_factory('({},' + filter_name + ',{})') + +def raw_template_filter_class_factory(template: str): + class WrappedFilter(WhereFilter): + def __init__(self, *args, **kwargs): + self.__filter = RawTemplateFilter(template, *args, **kwargs) + def get_where(self) -> str: + return self.__filter.get_where() + return WrappedFilter diff --git a/nocodb/filters/factory_test.py b/nocodb/filters/factory_test.py new file mode 100644 index 0000000..836aa11 --- /dev/null +++ b/nocodb/filters/factory_test.py @@ -0,0 +1,15 @@ +from .factory import basic_filter_class_factory, raw_template_filter_class_factory + + +def test_basic_filter_class_factory(): + FilterClass = basic_filter_class_factory('eq') + assert FilterClass('column', 'value').get_where() == '(column,eq,value)' + + +def test_raw_template_filter_class_factory(): + FilterClassWithoutParams = raw_template_filter_class_factory('()') + FilterClassWithParams = raw_template_filter_class_factory('({},{},{})') + FilterClassWithKwargs = raw_template_filter_class_factory('({},{op},{})') + assert FilterClassWithoutParams().get_where() == '()' + assert FilterClassWithParams('1', '2','3').get_where() == '(1,2,3)' + assert FilterClassWithKwargs('1', '2', op='eq').get_where() == '(1,eq,2)' diff --git a/nocodb/filters/filters_test.py b/nocodb/filters/filters_test.py new file mode 100644 index 0000000..bcc5f06 --- /dev/null +++ b/nocodb/filters/filters_test.py @@ -0,0 +1,20 @@ +import pytest + +from .. import filters + +from ..nocodb import WhereFilter + + +@pytest.mark.parametrize('filter_class, expected_operator', [ + (filters.EqFilter, 'eq'), + (filters.EqualFilter, 'eq'), + (filters.NotEqualFilter, 'neq'), + (filters.GreaterOrEqualFilter, 'ge'), + (filters.GreaterThanFilter, 'gt'), + (filters.LessThanFilter, 'lt'), + (filters.LessOrEqualFilter, 'le'), + (filters.LikeFilter, 'like') + ]) +def test_basic_filters_are_correctly_created(filter_class: WhereFilter, expected_operator: str): + test_filter = filter_class('column', 'value') + assert test_filter.get_where() == f'(column,{expected_operator},value)' diff --git a/nocodb/filters/raw_filter.py b/nocodb/filters/raw_filter.py new file mode 100644 index 0000000..451d659 --- /dev/null +++ b/nocodb/filters/raw_filter.py @@ -0,0 +1,19 @@ +from ..nocodb import WhereFilter + + +class RawFilter(WhereFilter): + def __init__(self, raw: str): + self.__raw = raw + + def get_where(self) -> str: + return self.__raw + + +class RawTemplateFilter(WhereFilter): + def __init__(self, template: str, *args, **kwargs): + self.__template = template + self.__template_values = args + self.__template_kvalues = kwargs + + def get_where(self) -> str: + return self.__template.format(*self.__template_values, **self.__template_kvalues) From 518a012326dfe9357e809c383c55951e8506b83b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez=20Saura?= Date: Thu, 27 Apr 2023 22:08:23 +0200 Subject: [PATCH 17/27] chore: update .gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index bee8a64..5ae5ec9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ __pycache__ +build/ +dist/ +*.egg-info +*.swp +*.pyc From e76099e2a778f582543f34ac8787c8fa4758c328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez=20Saura?= Date: Thu, 27 Apr 2023 22:51:47 +0200 Subject: [PATCH 18/27] feat: Add logical filters (AND,OR,NOT) --- README.md | 22 ++++++++++ nocodb/filters/__init__.py | 31 ++++++++++---- nocodb/filters/filters_test.py | 77 ++++++++++++++++++++++++++++------ nocodb/filters/logical.py | 34 +++++++++++++++ 4 files changed, 142 insertions(+), 22 deletions(-) create mode 100644 nocodb/filters/logical.py diff --git a/README.md b/README.md index fbf4439..9f1512c 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,28 @@ table_rows = client.table_row_list(project, table_name, ExactDateEqFilter('colum table_rows = client.table_row_list(project, table_name, ExactDateOpFilter('column', '2023-06-01', op='eq')) ``` +```python +from nocodb import filters + +# Basic filters... +nick_filter = filters.EqFilter("nickname", "elchicodepython") +country_filter = filters.EqFilter("country", "es") +girlfriend_code = filters.EqFilter("gfcode", "404") +current_mood_code = filters.EqFilter("moodcode", "418") + +# Combining filters using logical filters +or_filter = filters.Or(nick_filter, country_filter) +and_filter = filters.And(girlfriend_code, current_mood_code) + +# Negating filters with a Not filter +not_me = filters.Not(filters.EqFilter("nickname", "elchicodepython")) + +# You can also combine combinations +or_combined_filter = filters.Or(or_filter, and_filter) +and_combined_filter = filters.And(or_filter, and_filter) + +``` + Credits to @MitPitt for asking this feature. ## Author notes diff --git a/nocodb/filters/__init__.py b/nocodb/filters/__init__.py index fe57678..40a458b 100644 --- a/nocodb/filters/__init__.py +++ b/nocodb/filters/__init__.py @@ -1,12 +1,25 @@ -from ..nocodb import WhereFilter from .factory import basic_filter_class_factory +from .logical import And, Not, Or - -EqFilter = basic_filter_class_factory('eq') +EqFilter = basic_filter_class_factory("eq") EqualFilter = EqFilter -NotEqualFilter = basic_filter_class_factory('neq') -GreaterThanFilter = basic_filter_class_factory('gt') -GreaterOrEqualFilter = basic_filter_class_factory('ge') -LessThanFilter = basic_filter_class_factory('lt') -LessOrEqualFilter = basic_filter_class_factory('le') -LikeFilter = basic_filter_class_factory('like') +NotEqualFilter = basic_filter_class_factory("neq") +GreaterThanFilter = basic_filter_class_factory("gt") +GreaterOrEqualFilter = basic_filter_class_factory("ge") +LessThanFilter = basic_filter_class_factory("lt") +LessOrEqualFilter = basic_filter_class_factory("le") +LikeFilter = basic_filter_class_factory("like") + +__all__ = [ + "And", + "Not", + "Or", + "EqFilter", + "EqualFilter", + "NotEqualFilter", + "GreaterThanFilter", + "GreaterOrEqualFilter", + "LessThanFilter", + "LessOrEqualFilter", + "LikeFilter", +] diff --git a/nocodb/filters/filters_test.py b/nocodb/filters/filters_test.py index bcc5f06..eb17954 100644 --- a/nocodb/filters/filters_test.py +++ b/nocodb/filters/filters_test.py @@ -5,16 +5,67 @@ from ..nocodb import WhereFilter -@pytest.mark.parametrize('filter_class, expected_operator', [ - (filters.EqFilter, 'eq'), - (filters.EqualFilter, 'eq'), - (filters.NotEqualFilter, 'neq'), - (filters.GreaterOrEqualFilter, 'ge'), - (filters.GreaterThanFilter, 'gt'), - (filters.LessThanFilter, 'lt'), - (filters.LessOrEqualFilter, 'le'), - (filters.LikeFilter, 'like') - ]) -def test_basic_filters_are_correctly_created(filter_class: WhereFilter, expected_operator: str): - test_filter = filter_class('column', 'value') - assert test_filter.get_where() == f'(column,{expected_operator},value)' +@pytest.mark.parametrize( + "filter_class, expected_operator", + [ + (filters.EqFilter, "eq"), + (filters.EqualFilter, "eq"), + (filters.NotEqualFilter, "neq"), + (filters.GreaterOrEqualFilter, "ge"), + (filters.GreaterThanFilter, "gt"), + (filters.LessThanFilter, "lt"), + (filters.LessOrEqualFilter, "le"), + (filters.LikeFilter, "like"), + ], +) +def test_basic_filters_are_correctly_created( + filter_class: WhereFilter, expected_operator: str +): + test_filter = filter_class("column", "value") + assert test_filter.get_where() == f"(column,{expected_operator},value)" + + +def test_or_filter(): + nick_filter = filters.EqFilter("nickname", "elchicodepython") + country_filter = filters.EqFilter("country", "es") + nick_or_country_filter = filters.Or(nick_filter, country_filter) + assert ( + nick_or_country_filter.get_where() + == "((nickname,eq,elchicodepython)~or(country,eq,es))" + ) + + +def test_and_filter(): + nick_filter = filters.EqFilter("nickname", "elchicodepython") + country_filter = filters.EqFilter("country", "es") + nick_or_country_filter = filters.And(nick_filter, country_filter) + assert ( + nick_or_country_filter.get_where() + == "((nickname,eq,elchicodepython)~and(country,eq,es))" + ) + + +def test_combined_filter(): + nick_filter = filters.EqFilter("nickname", "elchicodepython") + country_filter = filters.EqFilter("country", "es") + girlfriend_code = filters.EqFilter("gfcode", "404") + current_mood_code = filters.EqFilter("moodcode", "418") + or_filter = filters.Or(nick_filter, country_filter) + and_filter = filters.And(girlfriend_code, current_mood_code) + or_combined_filter = filters.Or(or_filter, and_filter) + and_combined_filter = filters.And(or_filter, and_filter) + + assert ( + or_combined_filter.get_where() + == "(((nickname,eq,elchicodepython)~or(country,eq,es))~or((gfcode,eq,404)~and(moodcode,eq,418)))" + ) + assert ( + and_combined_filter.get_where() + == "(((nickname,eq,elchicodepython)~or(country,eq,es))~and((gfcode,eq,404)~and(moodcode,eq,418)))" + ) + + +def test_not_filter(): + me = filters.EqFilter("nickname", "elchicodepython") + not_me = filters.Not(me) + assert not_me.get_where() == "~not(nickname,eq,elchicodepython)" diff --git a/nocodb/filters/logical.py b/nocodb/filters/logical.py new file mode 100644 index 0000000..f3fbc8e --- /dev/null +++ b/nocodb/filters/logical.py @@ -0,0 +1,34 @@ +from typing import List +from ..nocodb import WhereFilter + + +class Or(WhereFilter): + def __init__(self, *filters: List[WhereFilter]): + self.__filters = filters + + def get_where(self) -> str: + return ( + "(" + + "~or".join([filter.get_where() for filter in self.__filters]) + + ")" + ) + + +class And(WhereFilter): + def __init__(self, *filters: List[WhereFilter]): + self.__filters = filters + + def get_where(self) -> str: + return ( + "(" + + "~and".join([filter.get_where() for filter in self.__filters]) + + ")" + ) + + +class Not(WhereFilter): + def __init__(self, filter: WhereFilter): + self.__filter = filter + + def get_where(self) -> str: + return "~not" + self.__filter.get_where() From b52c2c91bd660d5c76e218a0881040de6c192dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez=20Saura?= Date: Thu, 27 Apr 2023 22:56:42 +0200 Subject: [PATCH 19/27] chore: set version to 2.0.0a2 --- nocodb/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nocodb/__init__.py b/nocodb/__init__.py index 8c0d5d5..7e47739 100644 --- a/nocodb/__init__.py +++ b/nocodb/__init__.py @@ -1 +1 @@ -__version__ = "2.0.0" +__version__ = "2.0.0a2" diff --git a/setup.py b/setup.py index fc3574e..3e4cb60 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='nocodb', - version='1.0.1', + version='2.0.0a2', author='Samuel López Saura', author_email='samuellopezsaura@gmail.com', packages=find_packages(), From bfedebf01c3078f3de1753759c7f8121a49c4cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez=20Saura?= Date: Thu, 27 Apr 2023 23:44:05 +0200 Subject: [PATCH 20/27] chore: Remove deprecated comment and set version after tests to 2.0.0 --- README.md | 2 +- nocodb/__init__.py | 2 +- nocodb/nocodb.py | 23 ----------------------- setup.py | 2 +- 4 files changed, 3 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 9f1512c..51cdd7f 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ client.table_row_delete(project, table_name, row_id) ### Using custom filters -Nocodb is evolving and new operators are comming with each release. +Nocodb is evolving and new operators are coming with each release. Most of the basic operations are inside this package but you could need some new feature that could not be added yet. diff --git a/nocodb/__init__.py b/nocodb/__init__.py index 7e47739..8c0d5d5 100644 --- a/nocodb/__init__.py +++ b/nocodb/__init__.py @@ -1 +1 @@ -__version__ = "2.0.0a2" +__version__ = "2.0.0" diff --git a/nocodb/nocodb.py b/nocodb/nocodb.py index 26c3077..3d7eb07 100644 --- a/nocodb/nocodb.py +++ b/nocodb/nocodb.py @@ -54,29 +54,6 @@ def get_where(self) -> str: pass -"""This could be great but actually I don't know how to join filters in the -NocoDB DSL. I event don't know if this is possible through the current API. -I hope they add docs about it soon. - -class NocoDBWhere: - - def __init__(self): - self.__filter_array: List[WhereFilter] = [] - - def add_filter(self, where_filter: WhereFilter) -> NocoDBWhere: - self.__filter_array.append( - where_filter - ) - return self - - def get_where(self) -> str: - return '&'.join([filter_.get_where() for filter_ in self.__filter_array]) - - def __str__(self): - return f'Where: "{self.get_where()}"' -""" - - class NocoDBProject: def __init__(self, org_name: str, project_name: str): self.project_name = project_name diff --git a/setup.py b/setup.py index 3e4cb60..7b52b3e 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='nocodb', - version='2.0.0a2', + version='2.0.0', author='Samuel López Saura', author_email='samuellopezsaura@gmail.com', packages=find_packages(), From 83d63df992e9bf952aebf03901eca5b579716429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez?= Date: Fri, 28 Apr 2023 22:32:39 +0200 Subject: [PATCH 21/27] Fix misleading docs in readme --- README.md | 53 +++++++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 51cdd7f..e1398e8 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ pip install nocodb ### Client configuration ```python from nocodb.nocodb import NocoDBProject, APIToken, JWTAuthToken -from nocodb.filters import LikeFilter, EqFilter +from nocodb.filters import LikeFilter, EqFilter, And from nocodb.infra.requests_client import NocoDBRequestsClient @@ -96,9 +96,8 @@ table_rows = client.table_row_list(project, table_name, params={'limit': 10000}) table_rows = client.table_row_list(project, table_name, params={'offset': 100}) # Filter the query -# Currently only one filter at a time is allowed. I don't know how to join -# multiple conditions in nocodb dsl. If you know how please let me know :). table_rows = client.table_row_list(project, table_name, LikeFilter("name", "%sam%")) +table_rows = client.table_row_list(project, table_name, And(LikeFilter("name", "%sam%"), EqFilter("age", 26))) table_rows = client.table_row_list(project, table_name, filter_obj=EqFilter("Id", 100)) # Filter and count rows @@ -140,6 +139,33 @@ client.table_row_delete(project, table_name, row_id) - LessThanFilter - LessOrEqualFilter - LikeFilter +- Or +- Not +- And + +#### Combining filters using Logical operations + +```python +from nocodb import filters + +# Basic filters... +nick_filter = filters.EqFilter("nickname", "elchicodepython") +country_filter = filters.EqFilter("country", "es") +girlfriend_code = filters.EqFilter("gfcode", "404") +current_mood_code = filters.EqFilter("moodcode", "418") + +# Combining filters using logical filters +or_filter = filters.Or(nick_filter, country_filter) +and_filter = filters.And(girlfriend_code, current_mood_code) + +# Negating filters with a Not filter +not_me = filters.Not(filters.EqFilter("nickname", "elchicodepython")) + +# You can also combine combinations +or_combined_filter = filters.Or(or_filter, and_filter) +and_combined_filter = filters.And(or_filter, and_filter) + +``` ### Using custom filters @@ -185,27 +211,6 @@ table_rows = client.table_row_list(project, table_name, ExactDateEqFilter('colum table_rows = client.table_row_list(project, table_name, ExactDateOpFilter('column', '2023-06-01', op='eq')) ``` -```python -from nocodb import filters - -# Basic filters... -nick_filter = filters.EqFilter("nickname", "elchicodepython") -country_filter = filters.EqFilter("country", "es") -girlfriend_code = filters.EqFilter("gfcode", "404") -current_mood_code = filters.EqFilter("moodcode", "418") - -# Combining filters using logical filters -or_filter = filters.Or(nick_filter, country_filter) -and_filter = filters.And(girlfriend_code, current_mood_code) - -# Negating filters with a Not filter -not_me = filters.Not(filters.EqFilter("nickname", "elchicodepython")) - -# You can also combine combinations -or_combined_filter = filters.Or(or_filter, and_filter) -and_combined_filter = filters.And(or_filter, and_filter) - -``` Credits to @MitPitt for asking this feature. From c1d59ff2371dc6e77b62cf08ec34159e2bd7b4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez=20Saura?= Date: Fri, 28 Apr 2023 22:40:41 +0200 Subject: [PATCH 22/27] chore: Update version to fix the docs of the release --- nocodb/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nocodb/__init__.py b/nocodb/__init__.py index 8c0d5d5..159d48b 100644 --- a/nocodb/__init__.py +++ b/nocodb/__init__.py @@ -1 +1 @@ -__version__ = "2.0.0" +__version__ = "2.0.1" diff --git a/setup.py b/setup.py index 7b52b3e..fd1d200 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='nocodb', - version='2.0.0', + version='2.0.1', author='Samuel López Saura', author_email='samuellopezsaura@gmail.com', packages=find_packages(), From 9b069eed14cb76f868499b9f1ad3855d5dd3be3e Mon Sep 17 00:00:00 2001 From: ALEX Berlin <102976761+alex-berlin-tv@users.noreply.github.com> Date: Thu, 20 Jul 2023 22:19:50 +0200 Subject: [PATCH 23/27] Specify inheritance of APIToken and JWTAuthToken to AuthToken (#12) This omits the Pyright warning when an APIToken/JWAuthToken is used in the creation of a new NocoDBRequests instance. --- nocodb/nocodb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nocodb/nocodb.py b/nocodb/nocodb.py index 3d7eb07..1fe801e 100644 --- a/nocodb/nocodb.py +++ b/nocodb/nocodb.py @@ -32,7 +32,7 @@ def get_header(self) -> dict: pass -class APIToken: +class APIToken(AuthToken): def __init__(self, token: str): self.__token = token @@ -40,7 +40,7 @@ def get_header(self) -> dict: return {"xc-token": self.__token} -class JWTAuthToken: +class JWTAuthToken(AuthToken): def __init__(self, token: str): self.__token = token From 1c914808a3866ab5c6458b5370fe4aa945e50c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez?= Date: Tue, 8 Aug 2023 10:42:02 +0200 Subject: [PATCH 24/27] docs: Add an example of how to paginate to the Readme --- README.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e1398e8..1b6ac06 100644 --- a/README.md +++ b/README.md @@ -89,12 +89,28 @@ table_name = "tablename" # Retrieve a page of rows from a table table_rows = client.table_row_list(project, table_name) -# Retrieve the first 10000 rows -table_rows = client.table_row_list(project, table_name, params={'limit': 10000}) +# Retrieve the first 1000 rows +table_rows = client.table_row_list(project, table_name, params={'limit': 1000}) # Skip 100 rows table_rows = client.table_row_list(project, table_name, params={'offset': 100}) +``` + +⚠️ Seems that we can't retrieve more than 1000 rows at the same time but we can paginate + to retrieve all the rows from a table + +Pagination example + +```python +first_100_rows = client.table_row_list(project, table_name, params={'limit': 100}) +next_100_rows = client.table_row_list(project, table_name, params={'limit': 100, 'offset': 100}) +next_100_rows = client.table_row_list(project, table_name, params={'limit': 100, 'offset': 200}) +``` + +More row operations + +```python # Filter the query table_rows = client.table_row_list(project, table_name, LikeFilter("name", "%sam%")) table_rows = client.table_row_list(project, table_name, And(LikeFilter("name", "%sam%"), EqFilter("age", 26))) From 9e808e56f3b2dad3a81100959e124f343568da05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez?= Date: Sat, 21 Oct 2023 13:24:45 +0200 Subject: [PATCH 25/27] Create contributors.md --- contributors.md | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 contributors.md diff --git a/contributors.md b/contributors.md new file mode 100644 index 0000000..514fa6b --- /dev/null +++ b/contributors.md @@ -0,0 +1,81 @@ +# Contributor Guidelines for python-nocodb + +Welcome to python-nocodb! We're excited that you want to contribute to our open-source project. Please take a moment to read and follow these guidelines to ensure a smooth and collaborative development process. + +## Table of Contents + +- [Contribution Process](#contribution-process) + - [1. Fork the Repository](#1-fork-the-repository) + - [2. Create a Branch](#2-create-a-branch) + - [3. Work on Your Contribution](#3-work-on-your-contribution) + - [4. Test Your Code](#4-test-your-code) + - [5. Commit Your Changes](#5-commit-your-changes) + - [6. Create a Pull Request](#6-create-a-pull-request) +- [Coding Guidelines](#coding-guidelines) +- [Documentation](#documentation) +- [Authors](#authors) + +## Contribution Process + +To contribute to this project, follow these steps: + +### 1. Fork the Repository + +Click the "Fork" button on the top right corner of the repository's page on GitHub. This will create a copy of the repository in your GitHub account. + +### 2. Create a Branch + +Clone your forked repository to your local machine, then create a new branch for your contribution. Name your branch in a descriptive manner that reflects the purpose of your contribution. + +```bash +git clone https://github.com/your-username/python-nocodb.git +cd python-nocodb +git checkout -b feature/your-feature +``` + +### 3. Work on Your Contribution + +Now you can start working on your contribution. Be sure to follow the coding guidelines (mentioned below) and implement your changes or new features. + +### 4. Test Your Code + +Thoroughly test your code to ensure it works as expected. Make sure to write unit tests, if applicable. Your contribution should not introduce new bugs or regressions. + +### 5. Commit Your Changes + +Once you are satisfied with your work, commit your changes. Be sure to write clear and descriptive commit messages. + +```bash +git add . +git commit -m "Add a clear and concise commit message" +``` + +### 6. Create a Pull Request + +When your code is ready for review, push your changes to your forked repository, and then open a Pull Request (PR) to the main repository's `main` branch. In your PR description, provide a clear and detailed explanation of your contribution, including what the change does and why it is needed. + +Our team will review your PR, provide feedback, and merge it when it meets our quality and functionality standards. + +## Coding Guidelines + +Please adhere to the following coding guidelines: + +- Follow the style and conventions used in the existing codebase. +- Write clean, readable, and well-documented code. +- Keep code modular and DRY (Don't Repeat Yourself). +- Ensure your code is consistent with the project's coding standards. +- Include unit tests when adding new functionality. + +## Documentation + +Documentation is crucial for maintaining and improving our project. When you make a contribution, you should: + +- Update or add documentation to explain the new capabilities or changes you've made. +- Include comments within your code to explain complex logic or important decisions. +- If you add or change a feature, update the project's README to reflect these changes. + +## Authors + +We appreciate all contributors to our project. To give credit where it's due, please add your name and GitHub username to the "Authors" section of the README if it's not already there. + +Thank you for your interest in contributing to python-nocodb. We look forward to your contributions and collaborations. Happy coding! From 4a3d12362cbf4d7afdff5110397d26d16b63c3ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20L=C3=B3pez?= Date: Sat, 21 Oct 2023 13:28:20 +0200 Subject: [PATCH 26/27] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1b6ac06..51e2987 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ NocoDB is a great Airtable alternative. This client allows python developers to use NocoDB API in a simple way. +- [Contributors guidelines](contributors.md) + ## Installation ```bash From 56e68f3b1dd5fe3ed83d20e041d088c25a736b0d Mon Sep 17 00:00:00 2001 From: Fernando Cillero Date: Sat, 21 Oct 2023 14:41:15 +0200 Subject: [PATCH 27/27] feat: refactor logical filters and add tests (#17) --- nocodb/filters/logical.py | 14 +++----------- nocodb/filters/logical_test.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 nocodb/filters/logical_test.py diff --git a/nocodb/filters/logical.py b/nocodb/filters/logical.py index f3fbc8e..94379a2 100644 --- a/nocodb/filters/logical.py +++ b/nocodb/filters/logical.py @@ -7,11 +7,7 @@ def __init__(self, *filters: List[WhereFilter]): self.__filters = filters def get_where(self) -> str: - return ( - "(" - + "~or".join([filter.get_where() for filter in self.__filters]) - + ")" - ) + return f"({'~or'.join([filter.get_where() for filter in self.__filters])})" class And(WhereFilter): @@ -19,11 +15,7 @@ def __init__(self, *filters: List[WhereFilter]): self.__filters = filters def get_where(self) -> str: - return ( - "(" - + "~and".join([filter.get_where() for filter in self.__filters]) - + ")" - ) + return f"({'~and'.join([filter.get_where() for filter in self.__filters])})" class Not(WhereFilter): @@ -31,4 +23,4 @@ def __init__(self, filter: WhereFilter): self.__filter = filter def get_where(self) -> str: - return "~not" + self.__filter.get_where() + return f"~not{self.__filter.get_where()}" diff --git a/nocodb/filters/logical_test.py b/nocodb/filters/logical_test.py new file mode 100644 index 0000000..d6865b6 --- /dev/null +++ b/nocodb/filters/logical_test.py @@ -0,0 +1,21 @@ +from nocodb import filters + + +def test_or_with_two_filters(): + filter1 = filters.EqFilter("column1", "value1") + filter2 = filters.EqFilter("column2", "value2") + or_filter = filters.Or(filter1, filter2) + assert or_filter.get_where() == "((column1,eq,value1)~or(column2,eq,value2))" + + +def test_and_with_two_filters(): + filter1 = filters.And(filters.EqFilter("column1", "value1")) + filter2 = filters.And(filters.EqFilter("column2", "value2")) + and_filter = filters.And(filter1, filter2) + assert and_filter.get_where() == "(((column1,eq,value1))~and((column2,eq,value2)))" + + +def test_not_filter(): + filter = filters.EqFilter("column", "value") + not_filter = filters.Not(filter) + assert not_filter.get_where() == "~not(column,eq,value)"