Skip to content

Commit

Permalink
Merge pull request #29 from lastorel/dev
Browse files Browse the repository at this point in the history
Release Notion 2022-06-28
  • Loading branch information
lastorel authored Jul 28, 2022
2 parents c42496f + 7133f27 commit 28597ab
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 41 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Changelog

## v1.3.0

- [#27](https://github.com/lastorel/pytion/issues/27): Switched from `2022-02-22` to `2022-06-28` version of Notion API
- `Request()` (internal) method argument added
- [#27](https://github.com/lastorel/pytion/issues/27): Fix of parent object hierarchy
- [#27](https://github.com/lastorel/pytion/issues/27): `models.Block` now has non-empty `parent` attr
- `models.Database`: `is_inline` attr added
- `Notion()`: new optional arg `version` added to customize API interaction
- [#27](https://github.com/lastorel/pytion/issues/27): You must retrieve Page properties manually. `.get_page_properties` method added
- [#27](https://github.com/lastorel/pytion/issues/27): add support of `relation` type `Property`
- [#27](https://github.com/lastorel/pytion/issues/27): updates for `relation` type `PropertyValue`
- [#16](https://github.com/lastorel/pytion/issues/17): tests of Property model

### Breaking changes

- `Request()` method now looks for positional argument `api` for getting version (internal method)
- Page has title=`unknown` until you retrieve its properties
- `PropertyValue` with `relation` type now represents by list of `LinkTo` object instead of list of IDs
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ Client is built with its own object model based on API
So if you are using **notion.so** and want to automate some stuff with the original API, you're welcome!
You can read any available data, create basic models, and even work with databases.

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

_*does not use notion-sdk-py client_

See [Change Log](./CHANGELOG.md)

# Contents

1. [Quick Start](#quick-start)
Expand Down Expand Up @@ -146,6 +148,8 @@ There is a list of available methods for communicate with **api.notion.com**. Th

`.get_page_property(property_id, id_, limit)` - Retrieve a page property item.

`.get_page_properties(title_only, obj)` - Retrieve the title or all properties of current Page or Page `obj`

`.db_query(id_, limit, filter_, sorts)` - Query Database.

`.db_filter(...see desc...)` - Query Database.
Expand Down Expand Up @@ -191,6 +195,7 @@ There are classes **based on API** structures:
- use `.db_filter()` to get database content with filtering and/or sorting
- `Page` based on [Page object](https://developers.notion.com/reference/page)
- You can create object `Page.create(...)` and/or use `.page_create(...)` API method
- use `.get_page_properties()` to retrieve page title and other `PropertyValue`-s
- use `.page_update()` method to modify attributes or delete the page
- use `.get_block_children()` to get page content (without nested blocks) (it will be `BlockArray`)
- use `.get_block_children_recursive()` to get page content with nested blocks
Expand All @@ -206,10 +211,10 @@ There are classes **based on API** structures:
- You can retrieve more data about a User by his ID using `.get()`
- `Property` based on [Property object](https://developers.notion.com/reference/property-object)
- You can create object `Property.create(...)` while creating or editing database: `.db_create()` or `.db_update()`
- `formula`, `relation`, `rollup` type properties configuration is not supported
- `formula`, `rollup` type properties configuration is not supported
- `PropertyValue` based on [Property values](https://developers.notion.com/reference/property-value-object)
- You can create object `PropertyValue.create(...)` to set or edit page properties by `.page_create()` or `.page_update()`
- `files`, `relation`, `formula`, `rollup` type properties are not editable
- `files`, `formula`, `rollup` type properties are not editable

There are also useful **internal** classes:

Expand Down Expand Up @@ -283,6 +288,13 @@ Extension attributes are listed below in support matrix:

> API converts **toggle heading** Block to simple heading Block.
### Supported Property types

| Property type | Property type | Property Schema | Property Values | Property Item | Config attrs |
| --- | --- | --- | --- | --- | --- |
| `title` | rw | rw | rw | + | |
| in_progress... | rw | rw | rw | + | |

### Block creating examples

Create `paragraph` block object and add it to Notion:
Expand Down
44 changes: 39 additions & 5 deletions pytion/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@


class Notion(object):
def __init__(self, token: Optional[str] = None):
self.version = envs.NOTION_VERSION
self.session = Request(token=token)
def __init__(self, token: Optional[str] = None, version: Optional[str] = None):
"""
Creates main API object.
:param token: provide your integration API token. If None - find the file `token`
:param version: provide non hardcoded API version
"""
self.version = version if version else envs.NOTION_VERSION
self.session = Request(api=self, token=token)
logger.debug(f"API object created. Version {envs.NOTION_VERSION}")

def __len__(self):
Expand Down Expand Up @@ -212,13 +218,38 @@ def get_page_property(self, property_id: str, id_: Optional[str] = None, limit:
return None
if isinstance(id_, str) and "-" in id_:
id_ = id_.replace("-", "")
if self.obj:
if self.obj and not id_:
id_ = self.obj.id
property_obj = self.api.session.method(
method="get", path=self.name, id_=id_, after_path="properties/"+property_id, limit=limit
)
return Element(api=self.api, name=f"pages/{id_}/properties", obj=PropertyValue(property_obj, property_id))

def get_page_properties(self, title_only: bool = False, obj: Optional[Page] = None) -> None:
"""
Page properties must be retrieved using the page properties endpoint. (c)
after retrieving a Page object you can retrieve its properties
obj or self.obj must be a Page
:return:
"""
if not obj:
obj = self.obj
if obj and isinstance(obj, Page):
for prop in obj.properties:
# Skip already retrieved properties
if isinstance(obj.properties[prop], PropertyValue):
continue
prop_id = obj.properties[prop].id
if title_only and prop_id != "title":
continue
result = self.get_page_property(prop_id, id_=obj.id)
obj.properties[prop] = result.obj
if prop_id == "title":
obj.title = result.obj.value if result.obj.value else ""
return
logger.warning("You must provide a Page to retrieve properties")

def db_query(
self,
id_: Optional[str] = None,
Expand All @@ -240,7 +271,10 @@ def db_query(
)
if r["object"] != "list":
return None
return Element(api=self.api, name="pages", obj=PageArray(r["results"]))
pa = Element(api=self.api, name="pages", obj=PageArray(r["results"]))
for p in pa.obj:
pa.get_page_properties(title_only=True, obj=p)
return pa

def db_filter(self, title: str = None, **kwargs) -> Optional[Element]:
"""
Expand Down
2 changes: 1 addition & 1 deletion pytion/envs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
NOTION_SECRET = None

# Current API Version (mandatory)
NOTION_VERSION = "2022-02-22"
NOTION_VERSION = "2022-06-28"

# Logging settings (mandatory)
LOGGING_BASE_LEVEL = logging.WARNING
Expand Down
56 changes: 42 additions & 14 deletions pytion/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,23 @@ def format_iso_time(cls, time: str) -> Optional[datetime]:


class Property(object):
def __init__(self, data: Dict[str, str]):
self.to_delete = True if data.get("type") is None else False
def __init__(self, data: Dict[str, Any]):
self.to_delete = True if data.get("type", False) is None else False
self.id: str = data.get("id")
self.type: str = data.get("type", "")
self.name: str = data.get("name")
self.raw = data
self.subtype = None

if self.type == "relation":
if isinstance(data[self.type], dict):
self.subtype = data[self.type].get("type")
self.relation = LinkTo.create(database_id=data[self.type].get("database_id"))
if self.subtype == "single_property":
pass
elif self.subtype == "dual_property":
self.relation_property_id = data[self.type][self.subtype].get("synced_property_id")
self.relation_property_name = data[self.type][self.subtype].get("synced_property_name")

def __str__(self):
return self.name if self.name else self.type
Expand All @@ -237,7 +248,11 @@ def get(self) -> Optional[Dict[str, Dict]]:
data["name"] = self.name
# property retyping while patch
if self.type:
data[self.type] = {}
# create relation type property with configuration
if self.type == "relation":
data[self.type] = {self.subtype: {}, "database_id": self.relation.id}
else:
data[self.type] = {}
return data

@classmethod
Expand All @@ -248,7 +263,15 @@ def create(cls, type_: Optional[str] = "", **kwargs):
+ addons:
set type_ = `None` to delete this Property
set param `name` to rename this Property
+ relation type:
set param `single_property` with `database_id` value OR
set param `dual_property` with `database_id` value
Property.create(type_="relation", dual_property="v111c132c12c1242341c41c")
"""
if type_ == "relation":
subtype = next(kwarg for kwarg in kwargs if kwarg in ("single_property", "dual_property"))
kwargs["relation"] = {"type": subtype, subtype: {}, "database_id": kwargs[subtype]}
return cls({"type": type_, **kwargs})


Expand Down Expand Up @@ -331,7 +354,10 @@ def __init__(self, data: Dict, name: str, **kwargs):
self.value = [user if isinstance(user, User) else User(**user) for user in data[self.type]]

if self.type == "relation":
self.value: List[str] = [item.get("id") for item in data["relation"]]
self.value: List[LinkTo] = [
LinkTo.create(page_id=item.get("id")) if not isinstance(item, LinkTo) else item
for item in data[self.type]
]

if self.type == "rollup":
rollup_type = data["rollup"]["type"]
Expand Down Expand Up @@ -420,8 +446,12 @@ def get(self):
if self.type == "people":
return {self.type: [user.get() for user in self.value]}

# relation type
if self.type == "relation":
return {self.type: [{"id": lt.id} for lt in self.value]}

# unsupported types:
if self.type in ["files", "relation"]:
if self.type in ["files"]:
return {self.type: []}
if self.type in ["created_time", "last_edited_by", "last_edited_time", "created_by"]:
return None
Expand Down Expand Up @@ -450,6 +480,7 @@ def __init__(self, **kwargs) -> None:
:param properties:
:param parent:
:param url:
:param is_inline:
"""
super().__init__(**kwargs)
self.cover: Optional[Dict] = kwargs.get("cover")
Expand All @@ -464,6 +495,7 @@ def __init__(self, **kwargs) -> None:
}
self.parent = kwargs["parent"] if isinstance(kwargs.get("parent"), LinkTo) else LinkTo(**kwargs["parent"])
self.url: str = kwargs.get("url")
self.is_inline: bool = kwargs.get("is_inline")

def __str__(self):
return str(self.title)
Expand Down Expand Up @@ -507,15 +539,10 @@ def __init__(self, **kwargs) -> None:
self.url: str = kwargs.get("url")
self.children = kwargs["children"] if "children" in kwargs else LinkTo(block=self)
self.properties = {
name: (PropertyValue(data, name) if not isinstance(data, PropertyValue) else data)
name: (Property(data) if not isinstance(data, PropertyValue) else data)
for name, data in kwargs["properties"].items()
}
for p in self.properties.values():
if "title" in p.type:
self.title = p.value
break
else:
self.title = None
self.title = "unknown"

def __str__(self):
return str(self.title)
Expand Down Expand Up @@ -579,6 +606,7 @@ def __init__(self, **kwargs):
if isinstance(self.caption, str):
self.caption = RichTextArray.create(self.caption)
return
self.parent = kwargs["parent"] if isinstance(kwargs.get("parent"), LinkTo) else LinkTo(**kwargs["parent"])

if self.type == "paragraph":
self.text = RichTextArray(kwargs[self.type].get("rich_text"))
Expand Down Expand Up @@ -956,11 +984,11 @@ def __init__(
self.id = ""
self.after_path = ""
if self.type == "page_id":
self.uri = "blocks"
self.uri = "pages"
elif self.type == "database_id":
self.uri = "databases"
# when type is set manually
elif self.type == "page":
elif self.type == "page": # deprecated.
self.uri = "pages"
elif self.type == "block_id":
self.uri = "blocks"
Expand Down
3 changes: 2 additions & 1 deletion pytion/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def __repr__(self):
class Request(object):
def __init__(
self,
api: object, # Notion object
method: Optional[str] = None,
path: Optional[str] = None,
id_: str = "",
Expand All @@ -156,7 +157,7 @@ def __init__(
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.version = getattr(api, "version")
self.auth = {"Authorization": "Bearer " + self._token}
self.session.headers.update({"Notion-Version": self.version, **self.auth})
self.result = None
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="pytion",
version="1.2.3",
version="1.3.0",
author="Yegor Gomzin",
author_email="slezycmex@mail.ru",
description="Unofficial Python client for official Notion API",
Expand Down
4 changes: 3 additions & 1 deletion tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ def page_for_pages(no):

@pytest.fixture(scope="session")
def page_for_updates(no):
return no.pages.get("36223246a20e42df8f9b354ed1f11d75")
page = no.pages.get("36223246a20e42df8f9b354ed1f11d75")
page.get_page_properties()
return page
Loading

0 comments on commit 28597ab

Please sign in to comment.