Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release Notion 2022-06-28 #29

Merged
merged 3 commits into from
Jul 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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