From dd4f48149bd18e91c5edd1d4fb05d60591976d2a Mon Sep 17 00:00:00 2001 From: Elliana May Date: Sun, 28 Sep 2025 00:28:26 +0800 Subject: [PATCH 1/9] process create table statements --- __snapshots__/test_parser.ambr | 8 ++++++++ pynotiondb/mysql_query_parser.py | 15 +++++++++++++++ test_parser.py | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/__snapshots__/test_parser.ambr b/__snapshots__/test_parser.ambr index b29ec59..d2a6618 100644 --- a/__snapshots__/test_parser.ambr +++ b/__snapshots__/test_parser.ambr @@ -1,4 +1,12 @@ # serializer version: 1 +# name: test_create + dict({ + 'columns': dict({ + 'id': 'INT', + }), + 'table_name': 'table1', + }) +# --- # name: test_sql_parser[DELETE FROM table1 WHERE column1 = 'value1';] dict({ 'table_name': 'table1', diff --git a/pynotiondb/mysql_query_parser.py b/pynotiondb/mysql_query_parser.py index 8ac51f9..da3d2d8 100644 --- a/pynotiondb/mysql_query_parser.py +++ b/pynotiondb/mysql_query_parser.py @@ -3,6 +3,7 @@ EQ, And, Binary, + Create, Delete, Expression, Insert, @@ -128,6 +129,15 @@ def extract_delete_statement_info(self) -> dict: return {"table_name": table_name, "where_clause": where_clause} + def extract_create_statement_info(self) -> dict: + match: Create = self.statement + schema = match.this + + table_name = schema.this.text("this") + columns = {col.text("this"): col.kind.this.value for col in schema.expressions} + + return {"table_name": table_name, "columns": columns} + def extract_set_values(self, set_values_str: list[EQ]) -> list[dict]: set_values = [] # Split by 'AND', but not within quotes @@ -153,6 +163,9 @@ def parse(self) -> dict: if isinstance(self.statement, Delete): return self.extract_delete_statement_info() + if isinstance(self.statement, Create): + return self.extract_create_statement_info() + raise ValueError("Invalid SQL statement") def check_statement(self) -> tuple[bool, str]: @@ -164,5 +177,7 @@ def check_statement(self) -> tuple[bool, str]: return True, "update" if isinstance(self.statement, Delete): return True, "delete" + elif isinstance(self.statement, Create): + return True, "create" return False, "unknown" diff --git a/test_parser.py b/test_parser.py index 8e757f5..75d58e6 100644 --- a/test_parser.py +++ b/test_parser.py @@ -21,3 +21,11 @@ def test_sql_parser(sql: str, snapshot): assert ok snapshot.assert_match(parser.parse()) + + +def test_create(snapshot): + parser = MySQLQueryParser("CREATE TABLE table1 (id int);") + ok, typ = parser.check_statement() + assert ok + + snapshot.assert_match(parser.parse()) From 6aa127f3e79744b4d9f89c3510ed2013799c714d Mon Sep 17 00:00:00 2001 From: Elliana May Date: Sun, 28 Sep 2025 00:56:54 +0800 Subject: [PATCH 2/9] process sql --- pynotiondb/notion_api.py | 22 +++++++++ pyproject.toml | 1 + uv.lock | 98 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/pynotiondb/notion_api.py b/pynotiondb/notion_api.py index 7d64adb..87430b4 100644 --- a/pynotiondb/notion_api.py +++ b/pynotiondb/notion_api.py @@ -1,6 +1,7 @@ import logging import requests +from notion_client import Client from .exceptions import NotionAPIError from .mysql_query_parser import MySQLQueryParser @@ -8,6 +9,13 @@ logger = logging.getLogger(__name__) +def format_type(s: str): + if s == "INT": + return {} + else: + raise Exception(s) + + class NotionAPI: SEARCH = "https://api.notion.com/v1/search" PAGES = "https://api.notion.com/v1/pages" @@ -39,6 +47,7 @@ def __init__(self, token: str, databases: dict[str, str]) -> None: } self.session = requests.Session() self.session.headers.update(self.headers) + self.client = Client(auth=token) def request_helper(self, url: str, method: str = "GET", payload=None): response = self.session.request(method, url, json=payload) @@ -419,6 +428,16 @@ def update(self, query) -> None: payload=payload, ) + def create(self, query: str) -> None: + parsed_data = MySQLQueryParser(query).parse() + + return self.client.databases.create( + title=parsed_data["table_name"], + properties={ + col: format_type(typ) for col, typ in parsed_data["columns"].items() + }, + ) + def delete(self, query) -> None: parsed_data = MySQLQueryParser(query).parse() @@ -468,6 +487,9 @@ def execute(self, sql, val=None): elif to_do == "delete": self.delete(query) + elif to_do == "create": + return self.create(query) + else: raise ValueError("Unsupported operation") diff --git a/pyproject.toml b/pyproject.toml index 7744a46..eb10cf2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ classifiers = [ "Programming Language :: Python :: 3", ] dependencies = [ + "notion-client>=2.5.0", "requests>=2.0.0", "sqlglot>=18.2.0", ] diff --git a/uv.lock b/uv.lock index 51a5334..634d466 100644 --- a/uv.lock +++ b/uv.lock @@ -7,6 +7,43 @@ resolution-markers = [ "python_full_version < '3.8.1'", ] +[[package]] +name = "anyio" +version = "4.5.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, + { name = "idna", marker = "python_full_version < '3.9'" }, + { name = "sniffio", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/f9/9a7ce600ebe7804daf90d4d48b1c0510a4561ddce43a596be46676f82343/anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b", size = 171293, upload-time = "2024-10-13T22:18:03.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/b4/f7e396030e3b11394436358ca258a81d6010106582422f23443c16ca1873/anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f", size = 89766, upload-time = "2024-10-13T22:18:01.524Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "idna", marker = "python_full_version >= '3.9'" }, + { name = "sniffio", marker = "python_full_version >= '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + [[package]] name = "certifi" version = "2025.8.3" @@ -143,6 +180,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/c3/6f0e3896f193528bbd2b4d2122d4be8108a37efab0b8475855556a8c4afa/fancycompleter-0.11.1-py3-none-any.whl", hash = "sha256:44243d7fab37087208ca5acacf8f74c0aa4d733d04d593857873af7513cdf8a6", size = 11207, upload-time = "2025-05-26T12:59:09.857Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "anyio", version = "4.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -161,6 +236,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] +[[package]] +name = "notion-client" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/8c/b2f11904e1ef7a33338d3890fc50d30ffa2d0fca51b01b0a827d909b615a/notion_client-2.5.0.tar.gz", hash = "sha256:a03a0cc0502292193527af07a3bd13e40eb2b1083b60dcc7d733baad2d2729fd", size = 20865, upload-time = "2025-08-26T10:40:05.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/52/6b2a3081e1cb1123b1b28d5b1ebe88cb9e19b72264552dc707c1b265a1a7/notion_client-2.5.0-py2.py3-none-any.whl", hash = "sha256:d2efde1def1e7a3cc68cf4d2cab715571c71767852b26bf04474e9945d4a5903", size = 14348, upload-time = "2025-08-26T10:40:03.806Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -222,6 +309,7 @@ name = "pynotiondb" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "notion-client" }, { name = "requests", version = "2.32.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "sqlglot", version = "26.33.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, @@ -240,6 +328,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "notion-client", specifier = ">=2.5.0" }, { name = "requests", specifier = ">=2.0.0" }, { name = "sqlglot", specifier = ">=18.2.0" }, ] @@ -367,6 +456,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + [[package]] name = "sqlglot" version = "26.33.0" From 34ca7eaa984b68b9c9ce9d6356afad87d32b5749 Mon Sep 17 00:00:00 2001 From: Elliana May Date: Sun, 28 Sep 2025 16:36:56 +0800 Subject: [PATCH 3/9] add basic table creation test --- pyproject.toml | 1 + test_parser.py | 19 ++++++++++++++++++- uv.lock | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index eb10cf2..c3cdb1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ classifiers = [ dependencies = [ "notion-client>=2.5.0", "requests>=2.0.0", + "respx>=0.22.0", "sqlglot>=18.2.0", ] diff --git a/test_parser.py b/test_parser.py index 75d58e6..f2982c7 100644 --- a/test_parser.py +++ b/test_parser.py @@ -1,5 +1,9 @@ +import json + from pytest import mark +from respx import MockRouter +from pynotiondb import NotionAPI from pynotiondb.mysql_query_parser import MySQLQueryParser @@ -23,9 +27,22 @@ def test_sql_parser(sql: str, snapshot): snapshot.assert_match(parser.parse()) +create_sql = "CREATE TABLE table1 (id int);" + + def test_create(snapshot): - parser = MySQLQueryParser("CREATE TABLE table1 (id int);") + parser = MySQLQueryParser(create_sql) ok, typ = parser.check_statement() assert ok snapshot.assert_match(parser.parse()) + + +def test_notion(snapshot): + with MockRouter(base_url="https://api.notion.com/v1") as req: + call = req.post("/databases").respond(200, json={}) + notion = NotionAPI("", {"table1": "table1"}) + notion.execute(create_sql) + + assert call.called + assert snapshot == json.loads(req.calls.last.request.content) diff --git a/uv.lock b/uv.lock index 634d466..28fdaa3 100644 --- a/uv.lock +++ b/uv.lock @@ -312,6 +312,7 @@ dependencies = [ { name = "notion-client" }, { name = "requests", version = "2.32.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "respx" }, { name = "sqlglot", version = "26.33.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "sqlglot", version = "27.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] @@ -330,6 +331,7 @@ dev = [ requires-dist = [ { name = "notion-client", specifier = ">=2.5.0" }, { name = "requests", specifier = ">=2.0.0" }, + { name = "respx", specifier = ">=0.22.0" }, { name = "sqlglot", specifier = ">=18.2.0" }, ] @@ -456,6 +458,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "respx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" From 5109e53bddb46c30214d1d15a655f6bcfe216790 Mon Sep 17 00:00:00 2001 From: Elliana May Date: Sun, 28 Sep 2025 16:40:46 +0800 Subject: [PATCH 4/9] add title support --- __snapshots__/test_parser.ambr | 1 + pynotiondb/mysql_query_parser.py | 5 ++++- pynotiondb/notion_api.py | 10 +++++----- test_parser.py | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/__snapshots__/test_parser.ambr b/__snapshots__/test_parser.ambr index d2a6618..e915930 100644 --- a/__snapshots__/test_parser.ambr +++ b/__snapshots__/test_parser.ambr @@ -3,6 +3,7 @@ dict({ 'columns': dict({ 'id': 'INT', + 'title': 'title', }), 'table_name': 'table1', }) diff --git a/pynotiondb/mysql_query_parser.py b/pynotiondb/mysql_query_parser.py index da3d2d8..0a103b2 100644 --- a/pynotiondb/mysql_query_parser.py +++ b/pynotiondb/mysql_query_parser.py @@ -134,7 +134,10 @@ def extract_create_statement_info(self) -> dict: schema = match.this table_name = schema.this.text("this") - columns = {col.text("this"): col.kind.this.value for col in schema.expressions} + columns = { + col.text("this"): col.kind.args.get("kind", col.kind.this.value) + for col in schema.expressions + } return {"table_name": table_name, "columns": columns} diff --git a/pynotiondb/notion_api.py b/pynotiondb/notion_api.py index 87430b4..ee6c01b 100644 --- a/pynotiondb/notion_api.py +++ b/pynotiondb/notion_api.py @@ -12,6 +12,8 @@ def format_type(s: str): if s == "INT": return {} + elif s == "title": + return {} else: raise Exception(s) @@ -430,12 +432,10 @@ def update(self, query) -> None: def create(self, query: str) -> None: parsed_data = MySQLQueryParser(query).parse() - + props = {col: format_type(typ) for col, typ in parsed_data["columns"].items()} return self.client.databases.create( - title=parsed_data["table_name"], - properties={ - col: format_type(typ) for col, typ in parsed_data["columns"].items() - }, + title=[{"text": {"content": parsed_data["table_name"]}}], + properties=props, ) def delete(self, query) -> None: diff --git a/test_parser.py b/test_parser.py index f2982c7..fdc0e4e 100644 --- a/test_parser.py +++ b/test_parser.py @@ -27,7 +27,7 @@ def test_sql_parser(sql: str, snapshot): snapshot.assert_match(parser.parse()) -create_sql = "CREATE TABLE table1 (id int);" +create_sql = "CREATE TABLE table1 (title title, id int);" def test_create(snapshot): From e50988ae480add4572e35371d075de9188a36586 Mon Sep 17 00:00:00 2001 From: Elliana May Date: Sun, 28 Sep 2025 16:41:21 +0800 Subject: [PATCH 5/9] fully support types --- pynotiondb/notion_api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pynotiondb/notion_api.py b/pynotiondb/notion_api.py index ee6c01b..b79e6ba 100644 --- a/pynotiondb/notion_api.py +++ b/pynotiondb/notion_api.py @@ -9,11 +9,13 @@ logger = logging.getLogger(__name__) -def format_type(s: str): +def format_type(s: str) -> dict: if s == "INT": - return {} + return {"number": True} + elif s == "VARCHAR": + return {"rich_text": {}} elif s == "title": - return {} + return {"title": {}} else: raise Exception(s) From 73088b7f69c7d5d55cc00064546118d76f54aaa0 Mon Sep 17 00:00:00 2001 From: Elliana May Date: Sun, 28 Sep 2025 16:42:37 +0800 Subject: [PATCH 6/9] merge tests --- __snapshots__/test_parser.ambr | 22 +++++++++++++++++++++- test_parser.py | 14 +++----------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/__snapshots__/test_parser.ambr b/__snapshots__/test_parser.ambr index e915930..894a426 100644 --- a/__snapshots__/test_parser.ambr +++ b/__snapshots__/test_parser.ambr @@ -1,5 +1,25 @@ # serializer version: 1 -# name: test_create +# name: test_notion + dict({ + 'properties': dict({ + 'id': dict({ + 'number': True, + }), + 'title': dict({ + 'title': dict({ + }), + }), + }), + 'title': list([ + dict({ + 'text': dict({ + 'content': 'table1', + }), + }), + ]), + }) +# --- +# name: test_sql_parser[CREATE TABLE table1 (title title, id int);] dict({ 'columns': dict({ 'id': 'INT', diff --git a/test_parser.py b/test_parser.py index fdc0e4e..7e030a1 100644 --- a/test_parser.py +++ b/test_parser.py @@ -6,6 +6,8 @@ from pynotiondb import NotionAPI from pynotiondb.mysql_query_parser import MySQLQueryParser +create_sql = "CREATE TABLE table1 (title title, id int);" + @mark.parametrize( "sql", @@ -17,6 +19,7 @@ "DELETE FROM table1 WHERE column1 = 'value1';", "SELECT * FROM table1 WHERE column1=1 AND column2='text' OR column3 IS NULL;", "SELECT *, agg_list(column) FROM table GROUP BY column2 LIMIT 10 OFFSET 5;", + create_sql, ], ) def test_sql_parser(sql: str, snapshot): @@ -27,17 +30,6 @@ def test_sql_parser(sql: str, snapshot): snapshot.assert_match(parser.parse()) -create_sql = "CREATE TABLE table1 (title title, id int);" - - -def test_create(snapshot): - parser = MySQLQueryParser(create_sql) - ok, typ = parser.check_statement() - assert ok - - snapshot.assert_match(parser.parse()) - - def test_notion(snapshot): with MockRouter(base_url="https://api.notion.com/v1") as req: call = req.post("/databases").respond(200, json={}) From 9363cff7dd1a22c8ab97c09aac4cc1fb5f6263e3 Mon Sep 17 00:00:00 2001 From: Elliana May Date: Sun, 28 Sep 2025 16:45:06 +0800 Subject: [PATCH 7/9] add table_parent_page --- __snapshots__/test_parser.ambr | 3 +++ pynotiondb/notion_api.py | 13 ++++++++++++- test_parser.py | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/__snapshots__/test_parser.ambr b/__snapshots__/test_parser.ambr index 894a426..ee5fc4e 100644 --- a/__snapshots__/test_parser.ambr +++ b/__snapshots__/test_parser.ambr @@ -1,6 +1,9 @@ # serializer version: 1 # name: test_notion dict({ + 'parent': dict({ + 'database_id': 'PARENT_PAGE', + }), 'properties': dict({ 'id': dict({ 'number': True, diff --git a/pynotiondb/notion_api.py b/pynotiondb/notion_api.py index b79e6ba..a935b10 100644 --- a/pynotiondb/notion_api.py +++ b/pynotiondb/notion_api.py @@ -30,6 +30,7 @@ class NotionAPI: DEFAULT_PAGE_SIZE_FOR_SELECT_STATEMENTS = 20 token: str databases: dict[str, str] + table_parent_page: str | None CONDITION_MAPPING = { "EQ": "equals", @@ -39,9 +40,16 @@ class NotionAPI: ">=": "greater_than_or_equal_to", } - def __init__(self, token: str, databases: dict[str, str]) -> None: + def __init__( + self, + token: str, + databases: dict[str, str], + *, + table_parent_page: str | None = None, + ) -> None: self.token = token self.databases = databases + self.table_parent_page = table_parent_page self.DEFAULT_NOTION_VERSION = "2022-06-28" self.AUTHORIZATION = "Bearer " + self.token self.headers = { @@ -433,10 +441,13 @@ def update(self, query) -> None: ) def create(self, query: str) -> None: + if not self.table_parent_page: + raise Exception("Parent for new tables must be specified") parsed_data = MySQLQueryParser(query).parse() props = {col: format_type(typ) for col, typ in parsed_data["columns"].items()} return self.client.databases.create( title=[{"text": {"content": parsed_data["table_name"]}}], + parent={"database_id": self.table_parent_page}, properties=props, ) diff --git a/test_parser.py b/test_parser.py index 7e030a1..62e61a0 100644 --- a/test_parser.py +++ b/test_parser.py @@ -33,7 +33,7 @@ def test_sql_parser(sql: str, snapshot): def test_notion(snapshot): with MockRouter(base_url="https://api.notion.com/v1") as req: call = req.post("/databases").respond(200, json={}) - notion = NotionAPI("", {"table1": "table1"}) + notion = NotionAPI("", {"table1": "table1"}, table_parent_page="PARENT_PAGE") notion.execute(create_sql) assert call.called From ddfc222d05720a6fb02dbc123f049a5aa2aa2a83 Mon Sep 17 00:00:00 2001 From: Elliana May Date: Sun, 28 Sep 2025 18:32:03 +0800 Subject: [PATCH 8/9] handle IF NOT EXISTS --- __snapshots__/test_parser.ambr | 3 ++- pynotiondb/mysql_query_parser.py | 7 ++++++- pynotiondb/notion_api.py | 9 ++++++--- test_parser.py | 4 ++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/__snapshots__/test_parser.ambr b/__snapshots__/test_parser.ambr index ee5fc4e..602188d 100644 --- a/__snapshots__/test_parser.ambr +++ b/__snapshots__/test_parser.ambr @@ -22,12 +22,13 @@ ]), }) # --- -# name: test_sql_parser[CREATE TABLE table1 (title title, id int);] +# name: test_sql_parser[CREATE TABLE IF NOT EXISTS table1 (title title, id int);] dict({ 'columns': dict({ 'id': 'INT', 'title': 'title', }), + 'exists': True, 'table_name': 'table1', }) # --- diff --git a/pynotiondb/mysql_query_parser.py b/pynotiondb/mysql_query_parser.py index 0a103b2..1e40891 100644 --- a/pynotiondb/mysql_query_parser.py +++ b/pynotiondb/mysql_query_parser.py @@ -131,6 +131,7 @@ def extract_delete_statement_info(self) -> dict: def extract_create_statement_info(self) -> dict: match: Create = self.statement + assert match.kind == "TABLE" schema = match.this table_name = schema.this.text("this") @@ -139,7 +140,11 @@ def extract_create_statement_info(self) -> dict: for col in schema.expressions } - return {"table_name": table_name, "columns": columns} + return { + "table_name": table_name, + "columns": columns, + "exists": match.args.get("exists", False), + } def extract_set_values(self, set_values_str: list[EQ]) -> list[dict]: set_values = [] diff --git a/pynotiondb/notion_api.py b/pynotiondb/notion_api.py index a935b10..0c90995 100644 --- a/pynotiondb/notion_api.py +++ b/pynotiondb/notion_api.py @@ -1,4 +1,5 @@ import logging +from functools import lru_cache import requests from notion_client import Client @@ -29,7 +30,6 @@ class NotionAPI: QUERY_DATABASE = "https://api.notion.com/v1/databases/{}/query" DEFAULT_PAGE_SIZE_FOR_SELECT_STATEMENTS = 20 token: str - databases: dict[str, str] table_parent_page: str | None CONDITION_MAPPING = { @@ -43,12 +43,10 @@ class NotionAPI: def __init__( self, token: str, - databases: dict[str, str], *, table_parent_page: str | None = None, ) -> None: self.token = token - self.databases = databases self.table_parent_page = table_parent_page self.DEFAULT_NOTION_VERSION = "2022-06-28" self.AUTHORIZATION = "Bearer " + self.token @@ -183,6 +181,11 @@ def get_all_database_info(self, cursor=None, page_size=20): return data + @property + @lru_cache() + def databases(self): + return {db["title"]: db["id"] for db in self.get_all_database_info()["results"]} + def get_all_database(self): dbs = self.get_all_database_info() databases = [db.get("title") for db in dbs.get("results")] diff --git a/test_parser.py b/test_parser.py index 62e61a0..198f293 100644 --- a/test_parser.py +++ b/test_parser.py @@ -6,7 +6,7 @@ from pynotiondb import NotionAPI from pynotiondb.mysql_query_parser import MySQLQueryParser -create_sql = "CREATE TABLE table1 (title title, id int);" +create_sql = "CREATE TABLE IF NOT EXISTS table1 (title title, id int);" @mark.parametrize( @@ -33,7 +33,7 @@ def test_sql_parser(sql: str, snapshot): def test_notion(snapshot): with MockRouter(base_url="https://api.notion.com/v1") as req: call = req.post("/databases").respond(200, json={}) - notion = NotionAPI("", {"table1": "table1"}, table_parent_page="PARENT_PAGE") + notion = NotionAPI("", table_parent_page="PARENT_PAGE") notion.execute(create_sql) assert call.called From 8bbeee08365f730d8f1659146654a3bea1bcc477 Mon Sep 17 00:00:00 2001 From: Elliana May Date: Tue, 30 Sep 2025 14:18:27 +0800 Subject: [PATCH 9/9] fix syntax for py3.9 --- pynotiondb/notion_api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pynotiondb/notion_api.py b/pynotiondb/notion_api.py index 0c90995..98a6804 100644 --- a/pynotiondb/notion_api.py +++ b/pynotiondb/notion_api.py @@ -1,4 +1,5 @@ import logging +from typing import Optional from functools import lru_cache import requests @@ -30,7 +31,7 @@ class NotionAPI: QUERY_DATABASE = "https://api.notion.com/v1/databases/{}/query" DEFAULT_PAGE_SIZE_FOR_SELECT_STATEMENTS = 20 token: str - table_parent_page: str | None + table_parent_page: Optional[str] CONDITION_MAPPING = { "EQ": "equals", @@ -44,7 +45,7 @@ def __init__( self, token: str, *, - table_parent_page: str | None = None, + table_parent_page: Optional[str] = None, ) -> None: self.token = token self.table_parent_page = table_parent_page @@ -513,3 +514,4 @@ def execute(self, sql, val=None): raise ValueError( "Invalid SQL statement or type of statement not implemented" ) +