From 00ec9c95868b355107bd95588a3b14024ebf7ffb Mon Sep 17 00:00:00 2001 From: lastorel Date: Tue, 10 May 2022 14:44:29 +0300 Subject: [PATCH 1/4] fix #12. tests ok --- README.md | 4 +++- pytion/api.py | 4 ++-- pytion/models.py | 5 +++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0de9f65..c5a829e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ _*does not use notion-sdk-py client_ Create new integration and get your Notion API Token at notion.so -> [here](https://www.notion.com/my-integrations) -Invite your new integration 'manager' to your Notion workspace or particular pages. +Invite your new integration 'manager' to your pages or databases. `from pytion import Notion; no = Notion(token=SOME_TOKEN)` @@ -89,6 +89,8 @@ There are user classmethods for models: `RichTextArray.create()`, `Property.create()`, `PropertyValue.create()`, `Database.create()`, `Page.create()`, `Block.create()`, `LinkTo.create()`, `User.create()` +And every model has a `.get()` method that returns API friendly JSON. + ### Supported block types At present the API only supports the block types which are listed in the reference below. Any unsupported block types will continue to appear in the structure, but only contain a `type` set to `"unsupported"`. diff --git a/pytion/api.py b/pytion/api.py index e28f108..f8f6a6c 100644 --- a/pytion/api.py +++ b/pytion/api.py @@ -291,7 +291,7 @@ def db_create( """ :param database_obj: you can provide `Database` object or - provide the params for creating it: - :param parent: + :param parent: parent object in LinkTo format. workspace can not be a parent :param properties: dict of properties. Property with `title` type is mandatory! :param title: your name of the Database :return: self.obj -> Database @@ -366,7 +366,7 @@ def page_create( """ :param page_obj: you can provide `Page` object or - provide the params for creating it: - :param parent: LinkTo object with ID of parent element + :param parent: LinkTo object with ID of parent element. workspace can not be a parent :param properties: Dict of properties with values :param title: New title :param children: Content of new page in [Block] or BlockArray format diff --git a/pytion/models.py b/pytion/models.py index 91b666e..a927e9f 100644 --- a/pytion/models.py +++ b/pytion/models.py @@ -941,6 +941,8 @@ def __init__( else: self.type: str = kwargs.get("type") self.id: str = kwargs.get(self.type) if kwargs.get(self.type) else kwargs.get("id") + if self.type == "workspace": + self.id = "" self.after_path = "" if self.type == "page_id": self.uri = "blocks" @@ -973,6 +975,8 @@ def link(self) -> str: return NOTION_URL + str(self) def get(self, without_type: bool = False): + if self.type == "workspace": + return {"type": "workspace", "workspace": True} if without_type: return {self.type: self.id} return {"type": self.type, self.type: self.id} @@ -982,6 +986,7 @@ def create(cls, **kwargs): """ `.create(page_id="123412341234")` `.create(database_id="13412341234")` + `.create(workspace=True)` """ for key, value in kwargs.items(): return cls(type=key, id=value) From 137a6b60b1790cbe1162ba8af3a47b4012e88237 Mon Sep 17 00:00:00 2001 From: lastorel Date: Wed, 11 May 2022 00:02:37 +0300 Subject: [PATCH 2/4] `headers` attr of Request obj was removed --- pytion/query.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pytion/query.py b/pytion/query.py index 971e34c..1c65aeb 100644 --- a/pytion/query.py +++ b/pytion/query.py @@ -148,13 +148,14 @@ def __init__( sorts: Optional[Sort] = None, ): self.session = requests.Session() + self.session.headers["accept"] = "application/json" self.base = base if base else envs.NOTION_URL self._token = token if token else envs.NOTION_SECRET if not self._token: logger.error("Token is not provided or file `token` is not found!") self.version = envs.NOTION_VERSION self.auth = {"Authorization": "Bearer " + self._token} - self.headers = {"Notion-Version": self.version, **self.auth} + self.session.headers.update({"Notion-Version": self.version, **self.auth}) self.result = None if method: @@ -190,7 +191,7 @@ def method( logger.debug(f"METHOD: {method.upper()}") logger.debug(f"URL: {url}") logger.debug(f"DATA: {data}") - result = self.session.request(method=method, url=url, headers=self.headers, json=data) + result = self.session.request(method=method, url=url, json=data) logger.debug(f"STATUS CODE: {result.status_code}") logger.debug(f"CONTENT: {result.content}") logger.info(f"{result.status_code} Received") From b59f0448cc5fab9ca2267cec42aaf2f5503d8482 Mon Sep 17 00:00:00 2001 From: lastorel Date: Thu, 12 May 2022 00:10:07 +0300 Subject: [PATCH 3/4] #21 Block: `plain_text` renamed to `simple`. also added to RT, RTA, BlockArray --- README.md | 18 +++++++++------- pytion/models.py | 54 ++++++++++++++++++++++++++++++------------------ 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index c5a829e..0561016 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ Independent unofficial Python client for the official Notion API (for internal integrations only) -Supports Notion API version = **"2022-02-22"** +Client is built with own its object model based on API + +Current Notion API version = **"2022-02-22"** Works with **Python 3.8+** @@ -10,13 +12,15 @@ _*does not use notion-sdk-py client_ ## Quick start -`pip install pytion` - -Create new integration and get your Notion API Token at notion.so -> [here](https://www.notion.com/my-integrations) +``` +pip install pytion +``` -Invite your new integration 'manager' to your pages or databases. +Create new integration and get your Notion API Token at notion.so -> [here](https://www.notion.com/my-integrations). Invite your new integration 'manager' to your pages or databases. -`from pytion import Notion; no = Notion(token=SOME_TOKEN)` +``` +from pytion import Notion; no = Notion(token=SOME_TOKEN) +``` Or put your token for Notion API into file `token` at script directory and use simple `no = Notion()` @@ -108,7 +112,7 @@ Every Block has mandatory attributes and extension attributes. There are mandato - `has_children: bool` - does the block have children blocks (from API) - `archived: bool` - does the block marked as deleted (from API) - `text: Union[str, RichTextArray]` - **main content** -- `plain_text: str` - only simple text string +- `simple: str` - only simple text string (url expanded) Extension attributes are listed below in support matrix: diff --git a/pytion/models.py b/pytion/models.py index a927e9f..bb8688a 100644 --- a/pytion/models.py +++ b/pytion/models.py @@ -9,7 +9,6 @@ # I wanna use pydantic, but API provide variable names of property - class RichText(object): def __init__(self, **kwargs) -> None: self.plain_text: str = kwargs.get("plain_text") @@ -18,11 +17,13 @@ def __init__(self, **kwargs) -> None: # if not self.annotations: # self._create_default_annotations() self.type: str = kwargs.get("type") + self.simple = "" if self.type == "mention": subtype = kwargs[self.type].get("type") if subtype == "user": self.data = User(**kwargs[self.type].get(subtype)) self.plain_text = str(self.data) + self.simple = LinkTo(from_object=self.data).link elif subtype == "page": sub_id = kwargs[self.type][subtype].get("id") if kwargs[self.type].get(subtype) else "" self.data = LinkTo.create(page=sub_id) @@ -30,6 +31,7 @@ def __init__(self, **kwargs) -> None: self.plain_text = repr(self.data) else: self.plain_text = "LinkTo(" + self.plain_text + ")" + self.simple = self.data.link elif subtype == "database": sub_id = kwargs[self.type][subtype].get("id") if kwargs[self.type].get(subtype) else "" self.data = LinkTo.create(database_id=sub_id) @@ -37,16 +39,20 @@ def __init__(self, **kwargs) -> None: self.plain_text = repr(self.data) else: self.plain_text = "LinkTo(" + self.plain_text + ")" + self.simple = self.data.link elif subtype == "date": self.data = { "start": Model.format_iso_time(kwargs[self.type][subtype].get("start")), "end": Model.format_iso_time(kwargs[self.type][subtype].get("end")) } + self.simple = str(self.plain_text) elif subtype == "link_preview": + self.simple = str(self.plain_text) self.plain_text = f"<{self.plain_text}>" self.data: Dict = kwargs[self.type] else: self.data: Dict = kwargs[self.type] + self.simple = str(self.plain_text) def __str__(self): return str(self.plain_text) @@ -98,7 +104,7 @@ def insert(self, index: int, value) -> None: self.array.insert(index, value) def __str__(self): - return " ".join(str(rt) for rt in self) + return "".join(str(rt) for rt in self) def __repr__(self): return f"RichTextArray({str(self)})" @@ -119,6 +125,10 @@ def get(self) -> List[Dict[str, Any]]: def create(cls, text: str): return cls([{"type": "text", "plain_text": text, "text": {}}]) + @property + def simple(self) -> str: + return "".join(rt.simple for rt in self) + class User(object): """ @@ -576,48 +586,48 @@ def __init__(self, **kwargs): if self.type == "paragraph": self.text = RichTextArray(kwargs[self.type].get("rich_text")) - self._plain_text = str(self.text) + self._plain_text = self.text.simple elif "heading" in self.type: indent = self.type.split("_")[-1] indent_num = int(indent) if indent.isdigit() else 0 - prefix = "#" * indent_num + prefix = "#" * indent_num + " " r_text = RichTextArray(kwargs[self.type].get("rich_text")) self.text = RichTextArray.create(prefix) + r_text - self._plain_text = str(r_text) + self._plain_text = r_text.simple elif self.type == "callout": self.text = RichTextArray(kwargs[self.type].get("rich_text")) - self._plain_text = str(self.text) + self._plain_text = self.text.simple self.icon: Dict = kwargs[self.type].get("icon") elif self.type == "quote": r_text = RichTextArray(kwargs[self.type].get("rich_text")) - self.text = RichTextArray.create("|") + r_text - self._plain_text = str(r_text) + self.text = RichTextArray.create("| ") + r_text + self._plain_text = r_text.simple elif "list_item" in self.type: r_text = RichTextArray(kwargs[self.type].get("rich_text")) - self.text = RichTextArray.create("-") + r_text - self._plain_text = str(r_text) + self.text = RichTextArray.create("- ") + r_text + self._plain_text = r_text.simple # Numbers does not support cause of lack of relativity elif self.type == "to_do": self.checked: bool = kwargs[self.type].get("checked") - prefix = "[x]" if self.checked else "[ ]" + prefix = "[x] " if self.checked else "[ ] " r_text = RichTextArray(kwargs[self.type].get("rich_text")) self.text = RichTextArray.create(prefix) + r_text - self._plain_text = str(r_text) + self._plain_text = r_text.simple elif self.type == "toggle": r_text = RichTextArray(kwargs[self.type].get("rich_text")) - self.text = RichTextArray.create(">") + r_text - self._plain_text = str(r_text) + self.text = RichTextArray.create("> ") + r_text + self._plain_text = r_text.simple elif self.type == "code": r_text = RichTextArray(kwargs[self.type].get("rich_text")) self.text = RichTextArray.create("```\n") + r_text + "\n```" - self._plain_text = str(r_text) + self._plain_text = r_text.simple self.language: str = kwargs[self.type].get("language") self.caption = RichTextArray(kwargs[self.type].get("caption")) @@ -762,8 +772,8 @@ def __init__(self, **kwargs): elif self.type == "template": r_text = RichTextArray(kwargs[self.type].get("rich_text")) - self.text = RichTextArray.create("Template:") + r_text - self._plain_text = str(r_text) + self.text = RichTextArray.create("Template: ") + r_text + self._plain_text = r_text.simple elif self.type == "synced_block": synced_from = kwargs[self.type].get("synced_from") @@ -778,11 +788,11 @@ def __init__(self, **kwargs): elif self.type == "table_row": cells = kwargs[self.type].get("cells") - self.text = RichTextArray.create("|") + self.text = RichTextArray.create("| ") for cell in cells: text_cell = RichTextArray(cell) self._plain_text += f"\"{text_cell}\"," - self.text += text_cell + "|" + self.text += text_cell + " | " self._plain_text = self._plain_text.strip(",") elif self.type == "unsupported": @@ -822,7 +832,7 @@ def get(self, with_object_type: bool = False): return None @property - def plain_text(self) -> str: + def simple(self) -> str: if self._plain_text: return self._plain_text if self._plain_text != "None" else "" if getattr(self, "text", None): @@ -894,6 +904,10 @@ def __repr__(self): def get(self): return [b.get() for b in self] + @property + def simple(self) -> str: + return "\n".join(b._level * "\t" + b.simple for b in self) + class PageArray(ElementArray): def __repr__(self): From 821b633e293aee7a45d6812bd534021d1597f888 Mon Sep 17 00:00:00 2001 From: lastorel Date: Thu, 12 May 2022 01:21:49 +0300 Subject: [PATCH 4/4] docs updates --- README.md | 59 ++++++++++++++++++++++++++++++++---------------- pytion/models.py | 5 ++-- setup.py | 6 +++-- 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 0561016..3b5b9ae 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ # pytion -Independent unofficial Python client for the official Notion API (for internal integrations only) +[![PyPI](https://img.shields.io/pypi/v/pytion.svg)](https://pypi.org/project/pytion) +![PyVersion](https://img.shields.io/pypi/pyversions/pytion) +![CodeSize](https://img.shields.io/github/languages/code-size/lastorel/pytion) +[![LICENSE](https://img.shields.io/github/license/lastorel/pytion)](LICENSE) + +Independent unofficial **Python client** for the official **Notion API** (for internal integrations only) Client is built with own its object model based on API Current Notion API version = **"2022-02-22"** -Works with **Python 3.8+** - _*does not use notion-sdk-py client_ ## Quick start @@ -18,13 +21,13 @@ pip install pytion Create new integration and get your Notion API Token at notion.so -> [here](https://www.notion.com/my-integrations). Invite your new integration 'manager' to your pages or databases. -``` +```python from pytion import Notion; no = Notion(token=SOME_TOKEN) ``` Or put your token for Notion API into file `token` at script directory and use simple `no = Notion()` -``` +```python from pytion import Notion no = Notion(token=SOME_TOKEN) page = no.pages.get("PAGE ID") @@ -33,20 +36,38 @@ pages = database.db_filter(property_name="Done", property_type="checkbox", value ``` ``` -In [12]: no = Notion(token=SOME_TOKEN) +In [1]: from pytion import Notion + +In [2]: no = Notion(token=SOME_TOKEN) -In [13]: my_page = no.blocks.get("7584bc0bfb3b409cb17f957e51c9188a") +In [3]: page = no.pages.get("a458613160da45fa96367c8a594297c7") +In [4]: print(page) +Notion/pages/Page(Example page) -In [14]: blocks = my_page.get_block_children_recursive() +In [5]: blocks = page.get_block_children_recursive() -In [15]: print(blocks) -Notion/blocks/BlockArray(Heading 2 level Paragraph blo) +In [6]: print(blocks) +Notion/blocks/BlockArray(## Migration planning [x] Rese) -In [16]: print(blocks.obj) -Heading 2 level -Paragraph - block inside block -some text +In [7]: print(blocks.obj) +## Migration planning +[x] Reset new switch 2022-05-12T00:00:00.000+03:00 → 2022-05-13T01:00:00.000+03:00 + - reboot + - hold reset button +[x] Connect to console with baud rate 9600 +[ ] Skip default configuration dialog +Use LinkTo(configs) +[Integration changes](https://developers.notion.com/changelog?page=2) + +In [8]: print(blocks.obj.simple) +Migration planning +Reset new switch 2022-05-12T00:00:00.000+03:00 → 2022-05-13T01:00:00.000+03:00 + reboot + hold reset button +Connect to console with baud rate 9600 +Skip default configuration dialog +Use https://api.notion.com/v1/pages/90ea1231865f4af28055b855c2fba267 +https://developers.notion.com/changelog?page=2 ``` ## Available methods @@ -157,7 +178,7 @@ Extension attributes are listed below in support matrix: Create `paragraph` block object and add it to Notion: -``` +```python from pytion.models import Block my_text_block = Block.create("Hello World!") my_text_block = Block.create(text="Hello World!", type_="paragraph") # the same @@ -175,7 +196,7 @@ my_page.block_append(block=my_text_block) Create `to_do` block object: -``` +```python from pytion.models import Block my_todo_block = Block.create("create readme documentation", type_="to_do") my_todo_block2 = Block.create("add 'create' method", type_="to_do", checked=True) @@ -183,7 +204,7 @@ my_todo_block2 = Block.create("add 'create' method", type_="to_do", checked=True Create `code` block object: -``` +```python from pytion.models import Block my_code_block = Block.create("code example here", type_="code", language="javascript") my_code_block2 = Block.create("another code example", type_="code", caption="it will be plain text code block with caption") @@ -193,7 +214,7 @@ my_code_block2 = Block.create("another code example", type_="code", caption="it Logging is muted by default. To enable to stdout and/or to file: -``` +```python from pytion import setup_logging setup_logging(level="debug", to_console=True, filename="pytion.log") diff --git a/pytion/models.py b/pytion/models.py index bb8688a..f2e8e21 100644 --- a/pytion/models.py +++ b/pytion/models.py @@ -626,9 +626,10 @@ def __init__(self, **kwargs): elif self.type == "code": r_text = RichTextArray(kwargs[self.type].get("rich_text")) - self.text = RichTextArray.create("```\n") + r_text + "\n```" - self._plain_text = r_text.simple self.language: str = kwargs[self.type].get("language") + prefix = RichTextArray.create(f"```{self.language}\n") if self.language else RichTextArray.create("```\n") + self.text = prefix + r_text + "\n```" + self._plain_text = r_text.simple self.caption = RichTextArray(kwargs[self.type].get("caption")) # when the block is child_page, parent will be the page object diff --git a/setup.py b/setup.py index 2fdc32f..ca5c3ba 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="pytion", - version="1.2.1", + version="1.2.2", author="Yegor Gomzin", author_email="slezycmex@mail.ru", description="Unofficial Python client for official Notion API", @@ -16,7 +16,9 @@ "Bug Tracker": "https://github.com/lastorel/pytion/issues", }, classifiers=[ - "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: OS Independent", ],