Skip to content

Commit

Permalink
Merge pull request #22 from lastorel/dev-i21
Browse files Browse the repository at this point in the history
#21 fixes + #12 + docs
  • Loading branch information
lastorel authored May 11, 2022
2 parents 0281c67 + 821b633 commit 2550293
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 55 deletions.
83 changes: 55 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
# 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)

Supports Notion API version = **"2022-02-22"**
Independent unofficial **Python client** for the official **Notion API** (for internal integrations only)

Works with **Python 3.8+**
Client is built with own its object model based on API

Current Notion API version = **"2022-02-22"**

_*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 Notion workspace or particular pages.
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)`
```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")
Expand All @@ -29,20 +36,38 @@ pages = database.db_filter(property_name="Done", property_type="checkbox", value
```

```
In [12]: no = Notion(token=SOME_TOKEN)
In [13]: my_page = no.blocks.get("7584bc0bfb3b409cb17f957e51c9188a")
In [14]: blocks = my_page.get_block_children_recursive()
In [15]: print(blocks)
Notion/blocks/BlockArray(Heading 2 level Paragraph blo)
In [16]: print(blocks.obj)
Heading 2 level
Paragraph
block inside block
some text
In [1]: from pytion import Notion
In [2]: no = Notion(token=SOME_TOKEN)
In [3]: page = no.pages.get("a458613160da45fa96367c8a594297c7")
In [4]: print(page)
Notion/pages/Page(Example page)
In [5]: blocks = page.get_block_children_recursive()
In [6]: print(blocks)
Notion/blocks/BlockArray(## Migration planning [x] Rese)
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
Expand Down Expand Up @@ -89,6 +114,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"`.
Expand All @@ -106,7 +133,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:

Expand Down Expand Up @@ -151,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
Expand All @@ -169,15 +196,15 @@ 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)
```

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")
Expand All @@ -187,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")
Expand Down
4 changes: 2 additions & 2 deletions pytion/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
62 changes: 41 additions & 21 deletions pytion/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -18,35 +17,42 @@ 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)
if self.plain_text == "Untitled":
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)
if self.plain_text == "Untitled":
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)
Expand Down Expand Up @@ -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)})"
Expand All @@ -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):
"""
Expand Down Expand Up @@ -576,49 +586,50 @@ 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.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
Expand Down Expand Up @@ -762,8 +773,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")
Expand All @@ -778,11 +789,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":
Expand Down Expand Up @@ -822,7 +833,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):
Expand Down Expand Up @@ -894,6 +905,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):
Expand Down Expand Up @@ -941,6 +956,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"
Expand Down Expand Up @@ -973,6 +990,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}
Expand All @@ -982,6 +1001,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)
5 changes: 3 additions & 2 deletions pytion/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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")
Expand Down
Loading

0 comments on commit 2550293

Please sign in to comment.