Finaliza ajustes para iniciar Repositorio Git do SDK de Integração do Notion separado do meu projeto original

---

- Cria singleton de `client` com func `configure` para inicializar e `get_instance` para buscar instância do client;
- Ajusta clients para buscar headers vindo do pai `client` e fixa versão legacy no client de databases;
- Adiciona inicialização de `client` no init do projeto com api_token e api_version informados pelo usuário;
- Altera `NotionConfig` para inserir `database_id` no lugar de `database_name`;
- Altera sistema para receber `database_id` no lugar de `database_name`;
- Altera tipo de `properties` em `schemas.responses.pages.Page` de `Union[Dict[str, Any]], TDB` para `Union[Any, TDB]` para resolver reclamações de type hint;
- Adiciona param `generic_response` no init de `client` e nos clients e databases e pages para pular uso de mapping ao usar `.generic`;
- Adiciona param `raw_response` para pular parser e mappings e retornar resposta original da api;
- Finaliza `types` com subpastas para importações mas com init mãe vazio para evitar dependência circular e permitir uso de `notion.types.` pelo usuário;
- Remove importações do projeto original não relacionadas com o SDK;
- Adiciona param `timezone` na func `start_date` em `orm.common.SetProperty` que antes vinha do env, para posteriormente puxar da init da integração;
- Monta `LICENSE`, `README.md` e `pyproject.toml` base simples para commit inicial do projeto permitindo build de pacote;

---
This commit is contained in:
2026-01-20 22:26:39 -03:00
commit aff0446458
72 changed files with 2910 additions and 0 deletions
Executable
+10
View File
@@ -0,0 +1,10 @@
# Python Cache Files
**/__pycache__/
# Python Virtual Environment
.venv
# Environment Settings
.env
# VSCode Settings
.vscode
# Temp Folder
.temp/
Executable
+9
View File
@@ -0,0 +1,9 @@
# MIT License
Copyright 2026 Eduardo Riguetto (kralot / riguettodev)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Executable
+5
View File
@@ -0,0 +1,5 @@
# Notion SDK
**SDK para Integração com Notion API** com base forte em *pydantic*, *asyncio*, e programação orientada a objetos.
Pensado para o uso como *ORMs*. Permitindo personalização total do usuário ao configurar suas Databases do Notion, com recursos como mapeamentos, transformadores, validadores, calculos e mais.
+52
View File
@@ -0,0 +1,52 @@
from typing import Type, TypeVar, Generic, Union, Literal
from .auth import headers as _headers
from .schemas.orm.database.DatabasesContainer import DatabasesContainer as _DatabasesContainer
from .orm.repositories import _Repositories
from .client import Client as _Client
TContainer = TypeVar('TContainer', bound = _DatabasesContainer)
class Notion(Generic[TContainer]):
"Classe principal para configurar a integração Notion."
orm: '_ORM[TContainer]'
def __init__(self,
api_token : str,
api_version : Union[Literal["legacy", "data_sources"], str] = "data_sources",
orm_container : Type[TContainer] = _DatabasesContainer,
timezone : str = "Etc/UTC"
):
"""
### Integration Params
- **api_token** = Bearer Token de Integração com a API Notion.
- **api_version** = Seleção entre versão `legacy` com Databases e versão mais nova com `data_sources`, permitindo inserir versão personalizada. Valor padrão: `legacy` *(2022-06-28)*.
- **orm_container** = Databases Container com configuração de ORM personalizada com classe base de tipo `types.DatabasesContainer`
"""
headers = _headers(
api_token = api_token,
api_version = api_version
)
_Client.configure(headers)
self.client = _Client.get_instance()
self.orm = _ORM(databases_container = orm_container)
self.DatabasesContainer = _DatabasesContainer
class _ORM(Generic[TContainer]):
"Namespace ORM com tipo propagado"
repo: _Repositories[TContainer]
def __init__(self,
databases_container : Type[TContainer]
):
self.repo = _Repositories()
self.repo.databases.container = databases_container()
__all__ = ["Notion"]
+22
View File
@@ -0,0 +1,22 @@
from typing import Literal, Union
def headers(
api_token : str,
api_version : Union[Literal["legacy", "data_sources"], str] = "data_sources"
):
match api_version:
case "legacy":
version = "2022-06-28"
case "data_sources":
version = "2025-09-03"
case _:
version = api_version
headers = {
"Authorization" : f"Bearer {api_token}",
"Content-Type" : "application/json",
"Notion-Version" : version
}
return headers
+75
View File
@@ -0,0 +1,75 @@
from typing import Dict, Optional
from .blocks import Blocks
from .pages import Pages
from .databases import Databases
class Client:
"Client singleton da API Notion"
_headers : Optional[Dict[str, str]] = None
_instance : Optional['Client'] = None
def __init__(self):
self._blocks = None
self._pages = None
self._databases = None
@classmethod
def configure(cls, headers : Dict[str, str]):
"Configura o client com headers"
cls._headers = headers
@classmethod
def get_instance(cls) -> 'Client':
"Retorna a instância configurada"
if cls._instance is None:
cls._instance = Client()
return cls._instance
@property
def blocks(self) -> Blocks:
if self._blocks is None:
if Client._headers is None:
raise RuntimeError("Client não configurado. Instancie NotionIntegration primeiro.")
self._blocks = Blocks(Client._headers)
return self._blocks
@property
def pages(self) -> Pages:
if self._pages is None:
if Client._headers is None:
raise RuntimeError("Client não configurado. Instancie NotionIntegration primeiro.")
self._pages = Pages(Client._headers)
return self._pages
@property
def databases(self) -> Databases:
if self._databases is None:
if Client._headers is None:
raise RuntimeError("Client não configurado. Instancie NotionIntegration primeiro.")
self._databases = Databases(Client._headers)
return self._databases
def get_client() -> Client:
return Client.get_instance()
__all__ = ["Client", "get_client"]
+24
View File
@@ -0,0 +1,24 @@
import httpx
from typing import Dict
class Blocks:
"Reference: https://developers.notion.com/reference/retrieve-a-block"
def __init__(self, headers : Dict[str, str]):
self._headers = headers
async def get_children(self, page_id : str):
"Busca pelos blocos de uma página"
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
response = await client.get(
f'https://api.notion.com/v1/blocks/{page_id}/children',
headers = self._headers
)
return response.json()
__all__ = ["Blocks"]
+64
View File
@@ -0,0 +1,64 @@
import httpx
from typing import Dict
class Databases:
def __init__(self, headers : Dict[str, str]):
self._headers = {**headers, "Notion-Version": "2022-06-28"}
async def get(self, database_id):
"Buscar informações de um Banco de Dados"
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
response = await client.get(
f'https://api.notion.com/v1/databases/{database_id}',
headers = self._headers
)
return response.json()
async def query(self, database_id, json_data = {}):
"Buscar as Páginas de um Banco de Dados"
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
response = await client.post(
f'https://api.notion.com/v1/databases/{database_id}/query',
headers = self._headers,
json = json_data
)
return response.json()
async def query_propriety(self, database_id, propriety_type, json_data = {}):
"Buscar as Páginas de um Banco de Dados filtrando por uma Propriedade"
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
response = await client.post(
f'https://api.notion.com/v1/databases/{database_id}/query?filter_properties={propriety_type}',
headers = self._headers,
json = json_data
)
return response.json()
async def update(self, database_id, json_data):
"Atualiza as informações sobre um Banco de Dados"
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
response = await client.patch(
f'https://api.notion.com/v1/databases/{database_id}',
headers = self._headers,
json = json_data
)
return response.json()
__all__ = ["Databases"]
+73
View File
@@ -0,0 +1,73 @@
import httpx
from typing import Dict, Any
class Pages:
def __init__(self, headers : Dict[str, str]):
self._headers = headers
async def get(self,
page_id : str
):
"Buscar informações de uma Página"
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
response = await client.get(
f'https://api.notion.com/v1/pages/{page_id}',
headers = self._headers
)
return response.json()
async def get_property(self,
page_id : str,
property_name : str
):
"Buscar por informações de uma Propriedade em uma Página"
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
response = await client.get(
f'https://api.notion.com/v1/pages/{page_id}/properties/{property_name}',
headers = self._headers
)
return response.json()
async def update_properties(self,
page_id : str,
json_data : Dict[str, Any]
):
"Atualiza as Propriedades de uma Página"
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
response = await client.patch(
f'https://api.notion.com/v1/pages/{page_id}',
headers = self._headers,
json=json_data
)
return response.json()
async def create(self,
json_data : Dict[str, Any]
):
"Criar uma nova Página"
async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
response = await client.post(
f'https://api.notion.com/v1/pages',
headers = self._headers,
json = json_data
)
return response.json()
__all__ = ["Pages"]
+15
View File
@@ -0,0 +1,15 @@
from .common import Common as _common
from .parsers import Parser as _parser
from .mapping import Mapping as _mapping
from .repositories import repo as _repositories
class _NotionOrm:
def __init__(self) -> None:
self.common = _common
self.parser = _parser
self.mapping = _mapping
self.repo = _repositories
NotionOrm = _NotionOrm()
__all__ = ["NotionOrm"]
+106
View File
@@ -0,0 +1,106 @@
from typing import Any, Optional
from pydantic import validate_call
from ..extrators.Properties import PropertyExtractor
class _PageProperty(PropertyExtractor):
"Getter específico para propriedades individuais do Notion. Permite buscar uma propriedade específica por nome e tipo."
def _get_properties_dict(self, response: dict) -> dict:
"Extrai o dicionário de properties do response"
nt_dict = response.get('properties')
if nt_dict is None:
nt_dict = response.get('result')
if nt_dict is None:
raise KeyError("Response inserido é inválido")
return nt_dict
def _get_property_data(self, response: dict, name: str, expected_type: str) -> dict:
"""
Busca e valida uma propriedade específica.
Args:
response: Response completo do Notion
name: Nome da propriedade
expected_type: Tipo esperado da propriedade
Returns:
Dados da propriedade
Raises:
KeyError: Se propriedade não existe
ValueError: Se tipo não corresponde
"""
properties = self._get_properties_dict(response)
prop = properties.get(name)
if not prop:
raise KeyError(f"Propriedade '{name}' não foi encontrada")
actual_type = prop.get('type')
if actual_type != expected_type:
raise ValueError(
f"Propriedade '{name}' não é do tipo '{expected_type}' "
f"(tipo atual: '{actual_type}')"
)
return prop
# ==================== CLASSES INTERNAS ====================
@validate_call
def id(self, response: dict) -> str:
prop = response.get('id')
if not prop:
raise KeyError("ID da página não foi encontrada")
return prop
@validate_call
def title(self, response: dict, name: str) -> Optional[str]:
prop = self._get_property_data(response, name, "title")
return self._title(prop)
@validate_call
def text(self, response: dict, name: str) -> Optional[dict]:
prop = self._get_property_data(response, name, "rich_text")
return self._rich_text(prop)
@validate_call
def number(self, response: dict, name: str) -> Optional[float]:
prop = self._get_property_data(response, name, "number")
return self._number(prop)
@validate_call
def select(self, response: dict, name: str) -> Optional[dict]:
prop = self._get_property_data(response, name, "select")
return self._select(prop)
@validate_call
def checkbox(self, response: dict, name: str) -> bool:
prop = self._get_property_data(response, name, "checkbox")
return self._checkbox(prop)
@validate_call
def date(self, response: dict, name: str) -> Optional[dict]:
prop = self._get_property_data(response, name, "date")
return self._date(prop)
@validate_call
def relation(self, response: dict, name: str) -> Optional[list]:
prop = self._get_property_data(response, name, "relation")
return self._relation(prop)
@validate_call
def rollup(self, response: dict, name: str) -> Any:
prop = self._get_property_data(response, name, "rollup")
return self._rollup(prop)
@validate_call
def formula(self, response: dict, name: str) -> Any:
prop = self._get_property_data(response, name, "formula")
return self._formula(prop)
PageProperty = _PageProperty()
__all__ = ['_PageProperty']
+9
View File
@@ -0,0 +1,9 @@
from .PageProperty import PageProperty as _PageProperty
class _Acessors:
def __init__(self) -> None:
self.PageProperty = _PageProperty
Acessors = _Acessors
__all__ = ["Acessors"]
+12
View File
@@ -0,0 +1,12 @@
import json
from ..QueryFilter import QueryFilter
filter1 = QueryFilter.and_(
QueryFilter.title("Name", "contains", "Teste"),
QueryFilter.or_(
QueryFilter.number("Value", "greater_than", 10),
QueryFilter.number("Value", "less_than", 5)
)
)
print(json.dumps({"filter":filter1.to_dict()}))
+12
View File
@@ -0,0 +1,12 @@
import json
from ..QuerySort import QuerySort
sort1 = QuerySort.ascending("Teste")
print(json.dumps({"sorts":sort1.to_dict()}))
sort2 = QuerySort.and_(
QuerySort.ascending("Teste")
)
print(json.dumps({"sorts":sort2.to_dict()}))
+541
View File
@@ -0,0 +1,541 @@
from typing import Any, Dict, Literal, Union
from datetime import datetime, date
class _NotionFilter:
"Classe base para filtros do Notion"
def to_dict(self) -> Dict[str, Any]:
"Converte o filtro para o formato JSON do Notion"
raise NotImplementedError
def and_(self, *filters: '_NotionFilter') -> '_NotionFilter':
"Combina este filtro com outros usando AND"
return _AndFilter(self, *filters)
def or_(self, *filters: '_NotionFilter') -> '_NotionFilter':
"Combina este filtro com outros usando OR"
return _OrFilter(self, *filters)
class _PropertyFilter(_NotionFilter):
"Filtro para uma propriedade específica"
def __init__(self,
property_name: str,
property_type: str,
condition: str,
value: Any
):
self.property_name = property_name
self.property_type = property_type
self.condition = condition
self.value = value
def to_dict(self) -> Dict[str, Any]:
return {
"property": self.property_name,
self.property_type: {
self.condition: self.value
}
}
class _AndFilter(_NotionFilter):
"Combina múltiplos filtros com AND"
def __init__(self, *filters: _NotionFilter):
self.filters = list(filters)
def to_dict(self) -> Dict[str, Any]:
return {
"and": [filter_obj.to_dict() for filter_obj in self.filters]
}
class _OrFilter(_NotionFilter):
"Combina múltiplos filtros com OR"
def __init__(self, *filters: _NotionFilter):
self.filters = list(filters)
def to_dict(self) -> Dict[str, Any]:
return {
"or": [filter_obj.to_dict() for filter_obj in self.filters]
}
class QueryFilter:
"Builder principal para criar filtros"
@staticmethod
def and_(*filters: _NotionFilter) -> _AndFilter:
"Combina filtros com AND"
return _AndFilter(*filters)
@staticmethod
def or_(*filters: _NotionFilter) -> _OrFilter:
"Combina filtros com OR"
return _OrFilter(*filters)
@staticmethod
def created_time(
property_name: str,
condition: Literal[
"equals",
"before",
"after",
"on_or_before",
"on_or_after",
"is_empty",
"is_not_empty"
],
value: Union[date, Literal[True]]
):
if isinstance(value, date):
property_value = value.strftime("%Y-%m-%d")
else:
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "created_time",
condition = condition,
value = property_value
)
@staticmethod
def last_edited_time(
property_name: str,
condition: Literal[
"equals",
"before",
"after",
"on_or_before",
"on_or_after",
"is_empty",
"is_not_empty"
],
value: Union[date, Literal[True]]
):
if isinstance(value, date):
property_value = value.strftime("%Y-%m-%d")
else:
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "last_edited_time",
condition = condition,
value = property_value
)
@staticmethod
def title(
property_name: str,
condition: Literal[
"equals",
"does_not_equal",
"contains",
"does_not_contain",
"starts_with",
"ends_with",
"is_empty",
"is_not_empty"
],
value: str
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "title",
condition = condition,
value = property_value
)
@staticmethod
def rich_text(
property_name: str,
condition: Literal[
"equals",
"does_not_equal",
"contains",
"does_not_contain",
"starts_with",
"ends_with",
"is_empty",
"is_not_empty"
],
value: str
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "rich_text",
condition = condition,
value = property_value
)
@staticmethod
def number(
property_name: str,
condition: Literal[
"equals",
"does_not_equal",
"greater_than",
"less_than",
"greater_than_or_equal_to",
"less_than_or_equal_to",
"is_empty",
"is_not_empty"
],
value: Union[float, Literal[True]]
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "number",
condition = condition,
value = property_value
)
@staticmethod
def checkbox(
property_name: str,
condition: Literal[
"equals",
"does_not_equal"
],
value: Literal[True]
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "checkbox",
condition = condition,
value = property_value
)
@staticmethod
def select(
property_name: str,
condition: Literal[
"equals",
"does_not_equal",
"is_empty",
"is_not_empty"
],
value: Union[str, Literal[True]]
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "select",
condition = condition,
value = property_value
)
@staticmethod
def multi_select(
property_name: str,
condition: Literal[
"contains",
"does_not_contain",
"equals",
"does_not_equal",
"is_empty",
"is_not_empty"
],
value: Union[str, Literal[True]]
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "multi_select",
condition = condition,
value = property_value
)
@staticmethod
def status(
property_name: str,
condition: Literal[
"equals",
"does_not_equal",
"is_empty",
"is_not_empty"
],
value: Union[str, Literal[True]]
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "status",
condition = condition,
value = property_value
)
@staticmethod
def dates(
property_name: str,
condition: Literal[
"equals",
"does_not_equal",
"after",
"on_or_before",
"on_or_after",
"is_empty",
"is_not_empty"
],
value: Union[date, datetime, Literal[True]]
):
if isinstance(value, date):
property_value = value.strftime("%Y-%m-%d")
elif isinstance(value, datetime):
property_value = value.strftime("%Y-%m-%dT%H:%M:%S")
else:
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "date",
condition = condition,
value = property_value
)
@staticmethod
def people(
property_name: str,
condition: Literal[
"contains",
"does_not_contain",
"is_empty",
"is_not_empty"
],
value: Union[str, Literal[True]]
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "people",
condition = condition,
value = property_value
)
@staticmethod
def files(
property_name: str,
condition: Literal[
"is_empty",
"is_not_empty"
],
value: Literal[True]
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "files",
condition = condition,
value = property_value
)
@staticmethod
def url(
property_name: str,
condition: Literal[
"equals",
"does_not_equal",
"contains",
"does_not_contain",
"starts_with",
"ends_with",
"is_empty",
"is_not_empty"
],
value: Union[str, Literal[True]]
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "url",
condition = condition,
value = property_value
)
@staticmethod
def email(
property_name: str,
condition: Literal[
"equals",
"does_not_equal",
"contains",
"does_not_contain",
"starts_with",
"ends_with",
"is_empty",
"is_not_empty"
],
value: Union[str, Literal[True]]
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "email",
condition = condition,
value = property_value
)
@staticmethod
def phone_number(
property_name: str,
condition: Literal[
"equals",
"does_not_equal",
"contains",
"does_not_contain",
"starts_with",
"ends_with",
"is_empty",
"is_not_empty"
],
value: Union[str, Literal[True]]
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "phone_number",
condition = condition,
value = property_value
)
@staticmethod
def relation(
property_name: str,
condition: Literal[
"contains",
"does_not_contain",
"is_empty",
"is_not_empty"
],
value: Union[str, Literal[True]]
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "relation",
condition = condition,
value = property_value
)
@staticmethod
def created_by(
property_name: str,
condition: Literal[
"contains",
"does_not_contain",
"is_empty",
"is_not_empty"
],
value: Union[str, Literal[True]]
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "created_by",
condition = condition,
value = property_value
)
@staticmethod
def last_edited_by(
property_name: str,
condition: Literal[
"contains",
"does_not_contain",
"is_empty",
"is_not_empty"
],
value: Union[str, Literal[True]]
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "last_edited_by",
condition = condition,
value = property_value
)
@staticmethod
def formula(
property_name: str,
formula_type: Literal["string", "checkbox", "number", "date"],
condition: Literal[
"equals",
"does_not_equal",
"contains",
"does_not_contain",
"starts_with",
"ends_with",
"is_empty",
"is_not_empty",
"greater_than",
"less_than",
"greater_than_or_equal_to",
"less_than_or_equal_to",
"before",
"after",
"on_or_before",
"on_or_after"
],
value: Union[str, bool, float, date, datetime, Literal[True]]
) -> _PropertyFilter:
if isinstance(value, date):
property_value = value.strftime("%Y-%m-%d")
elif isinstance(value, datetime):
property_value = value.strftime("%Y-%m-%dT%H:%M:%S")
else:
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = f"formula.{formula_type}",
condition = condition,
value = property_value
)
@staticmethod
def unique_id(
property_name: str,
condition: Literal[
"equals",
"does_not_equal",
"greater_than",
"less_than",
"greater_than_or_equal_to",
"less_than_or_equal_to",
"is_empty",
"is_not_empty"
],
value: Union[float, Literal[True]]
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "unique_id",
condition = condition,
value = property_value
)
@staticmethod
def verification(
property_name: str,
condition: Literal[
"status"
],
value: Literal["verified", "expired", "none", ""]
):
property_value = value
return _PropertyFilter(
property_name = property_name,
property_type = "verification",
condition = condition,
value = property_value
)
__all__ = ["QueryFilter"]
+72
View File
@@ -0,0 +1,72 @@
from typing import Any, Dict, Literal, List
class _NotionSort:
"Classe base para classificação do Notion"
def _sort_to_dict(self) -> Dict[str, Any]:
"Converte uma classificação para o formato JSON do Notion"
raise NotImplementedError
def to_dict(self) -> List[Dict[str, Any]]:
"Converte uma ou multiplas classificações para o formato JSON do Notion"
raise NotImplementedError
class _PropertySort(_NotionSort):
"Classificação para uma propriedade específica"
def __init__(self,
property_name: str,
direction: Literal["ascending", "descending"]
):
self.property_name = property_name
self.direction = direction
def _sort_to_dict(self) -> Dict[str, Any]:
return {
"property": self.property_name,
"direction": self.direction
}
def to_dict(self) -> List[Dict[str, Any]]:
return [self._sort_to_dict()]
class _MultiSort(_NotionSort):
"Combina múltiplas classificações com vírgula"
def __init__(self, *sorts: _NotionSort):
self.sorts = sorts
def to_dict(self) -> List[Dict[str, Any]]:
return [sort_obj._sort_to_dict() for sort_obj in self.sorts]
class QuerySort:
"Builder principal para criar filtros"
@staticmethod
def and_(*sorts: _NotionSort) -> _MultiSort:
"Combina classificações com vírgula"
return _MultiSort(*sorts)
@staticmethod
def ascending(
property_name: str
):
return _PropertySort(
property_name = property_name,
direction = "ascending"
)
@staticmethod
def descending(
property_name: str
):
return _PropertySort(
property_name = property_name,
direction = "descending"
)
__all__ = ["QuerySort"]
+231
View File
@@ -0,0 +1,231 @@
from typing import Dict, Any, Union, Optional, Literal, List, TYPE_CHECKING
from datetime import datetime, date
from pydantic import validate_call
if TYPE_CHECKING:
from ..repositories.pages.CreatePage import CreatePage
from ..repositories.databases.CreateDatabasePage import CreateDatabasePage
class SetProperty:
def __init__(self,
CreatePageClass : Union['CreatePage', 'CreateDatabasePage'],
properties : Dict[str, Any]
) -> None:
self._CreatePage = CreatePageClass
self._properties = properties
@validate_call
def number(self,
name : str,
value : Optional[float]
):
if value is None:
return self._CreatePage
self._properties[name] = {
"number": value
}
return self._CreatePage
@validate_call
def checkbox(self,
name : str,
value : Optional[bool]
):
if value is None:
return self._CreatePage
self._properties[name] = {
"checkbox": value
}
return self._CreatePage
@validate_call
def start_date(self,
name : str,
value : Optional[Union[date, datetime]],
timezone : str = "Etc/UTC"
):
if value is None:
return self._CreatePage
if type(value) == date:
self._properties[name] = {
"date": {
"start": value.strftime("%Y-%m-%d")
}
}
elif type(value) == datetime:
self._properties[name] = {
"date": {
"start": value.strftime("%Y-%m-%dT%H:%M:%S"),
"time_zone": timezone
}
}
return self._CreatePage
@validate_call
def end_date(self,
name : str,
value : Optional[Union[date, datetime]]
):
if value is None:
return self._CreatePage
if not self._properties.get(name):
raise KeyError("start_date is missing")
date_value = None
if type(value) == date:
date_value = value.strftime("%Y-%m-%d")
elif type(value) == datetime:
date_value = value.strftime("%Y-%m-%dT%H:%M:%S")
self._properties[name]["date"]["end"] = date_value
return self._CreatePage
@validate_call
def relation(self,
name : str,
value : Optional[str]
):
if value is None:
return self._CreatePage
self._properties[name] = {
"relation": [
{
"id": str(value)
}
]
}
return self._CreatePage
@validate_call
def text(self,
name : str,
value : Optional[str]
):
if value is None:
return self._CreatePage
self._properties[name] = {
"rich_text" : [
{
"type":"text",
"text":{
"content": value
}
}
]
}
return self._CreatePage
@validate_call
def rich_text(self,
name : str,
value : Optional[Union[Dict[str, Any], List[Dict[str, Any]]]]
):
if value is None:
return self._CreatePage
value = [value] if isinstance(value, dict) else value
self._properties[name] = {
"rich_text" : value
}
return self._CreatePage
@validate_call
def select(self,
name : str,
value : Optional[str],
color : Optional[Literal[
"default",
"gray",
"brown",
"orange",
"yellow",
"green",
"blue",
"purple",
"pink",
"red"
]] = None
):
if value is None:
return self._CreatePage
if color:
self._properties[name] = {
"select" : {
"name" : value,
"color" : color
}
}
return self._CreatePage
self._properties[name] = {
"select" : {
"name" : value
}
}
return self._CreatePage
# @validate_call
# def multi_select(self, name : str, value : str):
# @validate_call
# def status(self, name : str, value : str):
# @validate_call
# def media(self, name : str, value : str):
@validate_call
def url(self,
name : str,
value : Optional[str]
):
if value is None:
return self._CreatePage
self._properties[name] = {
"url" : value
}
return self._CreatePage
@validate_call
def email(self,
name : str,
value : Optional[str]
):
if value is None:
return self._CreatePage
if "@" not in value:
raise ValueError("Invalid email")
self._properties[name] = {
"email": str(value)
}
return self._CreatePage
@validate_call
def phone(self,
name : str,
value : Optional[str]
):
if value is None:
return self._CreatePage
self._properties[name] = {
"phone_number": str(value)
}
return self._CreatePage
@validate_call
def person(self,
name : str,
value : Optional[str]
):
if value is None:
return self._CreatePage
self._properties[name] = {
"people": [
{
"object": "user",
"id": str(value)
}
]
}
return self._CreatePage
# @validate_call
# def place(self, name : str, value : str):
__all__ = ["SetProperty"]
+13
View File
@@ -0,0 +1,13 @@
from .QueryFilter import QueryFilter as _QueryFilter
from .QuerySort import QuerySort as _QuerySort
from .SetProperty import SetProperty as _SetProperty
class _Common:
def __init__(self) -> None:
self.QueryFilter = _QueryFilter
self.QuerySort = _QuerySort
self.SetProperty = _SetProperty
Common = _Common()
__all__ = ["Common"]
+146
View File
@@ -0,0 +1,146 @@
from typing import Optional, Any
from datetime import datetime
class PropertyExtractor:
"Classe base com métodos de extração de propriedades do Notion"
def extract(self, prop_data: dict) -> Any:
"""
Extrai o valor de uma propriedade baseado no seu tipo.
Args:
prop_data: Dicionário com dados da propriedade do Notion
Returns:
Valor extraído da propriedade ou None
"""
tipo = prop_data.get("type")
if not tipo:
return None
extractors = {
"title" : self._title,
"rich_text" : self._rich_text,
"number" : self._number,
"checkbox" : self._checkbox,
"url" : self._url,
"select" : self._select,
"multi_select" : self._multi_select,
"date" : self._date,
"relation" : self._relation,
"rollup" : self._rollup,
"formula" : self._formula,
}
extractor = extractors.get(tipo)
if extractor:
return extractor(prop_data)
return None
def _title(self, prop_data: dict) -> Optional[str]:
"Extrai conteúdo de propriedade tipo `title`"
title = prop_data.get("title")
if title is None:
return None
if isinstance(title, list) and len(title) == 1:
return title[0]['text'].get('content')
if isinstance(title, dict):
return title['text'].get('content')
content_list = [item['plain_text'] for item in title]
return "".join(content_list)
def _rich_text(self, prop_data: dict) -> Optional[dict]:
"Extrai conteúdo de propriedade tipo 'rich_text'"
prop_list = prop_data.get('rich_text')
if prop_list is None:
return None
content_list = [item['plain_text'] for item in prop_list]
return {"text": "".join(content_list), "detailed": prop_list}
def _number(self, prop_data: dict) -> Optional[float]:
"Extrai conteúdo de propriedade tipo 'number'"
return prop_data.get("number")
def _checkbox(self, prop_data: dict) -> bool:
"Extrai conteúdo de propriedade tipo 'checkbox'"
return prop_data.get("checkbox") == True
def _url(self, prop_data: dict) -> Optional[str]:
"Extrai conteúdo de propriedade tipo 'url'"
return prop_data.get("url")
def _select(self, prop_data: dict) -> Optional[dict]:
"Extrai conteúdo de propriedade tipo 'select'"
select = prop_data.get("select")
if not select:
return None
return {"name": select.get("name"), "color": select.get("color")}
def _multi_select(self, prop_data: dict) -> Optional[list[dict]]:
"Extrai conteúdo de propriedade tipo 'multi_select'"
multi_select = prop_data.get("multi_select")
if not multi_select:
return None
selects = []
for select in multi_select:
selects.append(
{"name": select.get("name"), "color": select.get("color")}
)
return selects
def _date(self, prop_data: dict) -> Optional[dict]:
"Extrai conteúdo de propriedade tipo 'date'"
date = prop_data.get("date")
if not date:
return None
def parse_date(date_str):
if not date_str:
return None
for fmt in ["%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d"]:
try:
return datetime.strptime(date_str, fmt)
except:
continue
return None
start = parse_date(date.get("start"))
end = parse_date(date.get("end"))
return {"start": start, "end": end}
def _relation(self, prop_data: dict) -> Optional[list]:
"Extrai conteúdo de propriedade tipo 'relation'"
relations = prop_data.get("relation", [])
if not relations:
return None
return [relation["id"] for relation in relations]
def _rollup(self, prop_data: dict) -> Any:
"Extrai conteúdo de propriedade tipo 'rollup'"
rollup = prop_data.get("rollup", {})
rollup_type = rollup.get("type")
if rollup_type == "number":
return rollup.get("number")
elif rollup_type == "array":
return rollup.get("array", [])
return None
def _formula(self, prop_data: dict) -> Any:
"Extrai conteúdo de propriedade tipo 'formula'"
formula = prop_data.get("formula", {})
formula_type = formula.get("type")
if formula_type == "number":
return formula.get("number")
elif formula_type == "string":
return formula.get("string")
elif formula_type == "boolean":
return formula.get("boolean")
return None
__all__ = ["PropertyExtractor"]
+8
View File
@@ -0,0 +1,8 @@
from .Properties import PropertyExtractor as _PropertyExtractor
class Extrator:
def __init__(self) -> None:
self.props = _PropertyExtractor
__all__ = ["Extrator"]
+12
View File
@@ -0,0 +1,12 @@
from .database import NotionDatabase as _NotionDatabase
from .registry import DatabaseRegistry as _DatabaseRegistry
class _Mapping:
NotionDatabase = _NotionDatabase
def __init__(self) -> None:
self.registry = _DatabaseRegistry
Mapping = _Mapping()
__all__ = ["Mapping"]
+198
View File
@@ -0,0 +1,198 @@
from typing import Dict, Any, Optional, Callable, ClassVar, TypeVar, Type
from pydantic import BaseModel, field_validator
T = TypeVar('T', bound='NotionDatabase')
class NotionConfigMeta:
"Metadados de configuração da database do Notion"
def __init__(self, config_class):
self.database_id : Optional[str] = getattr(config_class, 'database_id', None)
self.mappings : Dict[str, Any] = getattr(config_class, 'mappings', {})
self.validators : Dict[str, Callable] = getattr(config_class, 'validators', {})
self.computed : Dict[str, Callable] = getattr(config_class, 'computed', {})
transformers_config: Dict[str, Callable] = getattr(config_class, 'transformers', {})
# Processa mappings para separar nome e transformer
self.field_mappings : Dict[str, str] = {}
self.transformers : Dict[str, Callable] = {}
for field_name, mapping in self.mappings.items():
if isinstance(mapping, str):
# Mapping simples: "field": "Notion Name"
self.field_mappings[field_name] = mapping
elif isinstance(mapping, tuple) and len(mapping) == 2:
# Mapping com transformer: "field": ("Notion Name", lambda x: ...)
self.field_mappings[field_name] = mapping[0]
self.transformers[field_name] = mapping[1]
else:
raise ValueError(
f"Mapping inválido para '{field_name}'. "
f"Use: 'Notion Name' ou ('Notion Name', transformer_func)"
)
self.transformers.update(transformers_config)
class NotionDatabaseMeta(type(BaseModel)):
"Metaclass que processa a classe e injeta funcionalidades do Notion"
_notion_config: NotionConfigMeta
def __new__(mcs, name, bases, namespace, **kwargs):
# Cria a classe Pydantic
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
# Se tem NotionConfig, processa
if hasattr(cls, 'NotionConfig'):
config = NotionConfigMeta(cls.NotionConfig)
# Injeta o config processado
cls._notion_config = config # type: ignore
if not config.database_id:
raise AttributeError("Database ID is missing")
# Injeta validators customizados do NotionConfig
if config.validators:
for field_name, validator_func in config.validators.items():
# Cria um validator Pydantic dinâmico
validator_name = f'validate_{field_name}_notion'
def make_validator(func):
def validator(cls, v):
return func(v)
return field_validator(field_name)(validator)
setattr(cls, validator_name, make_validator(validator_func))
return cls
class NotionDatabase(BaseModel, metaclass = NotionDatabaseMeta):
"""
Classe base para schemas de databases do Notion.
Uso:
----
class AccountsDB(NotionDatabase):
name: str
credit: float
type: List[str]
class NotionConfig:
mappings = {
"name": "Name",
"credit": "Credit",
"type": ("Type", lambda x: [s["name"] for s in x] if x else [])
}
"""
_notion_config: ClassVar[NotionConfigMeta]
@classmethod
def from_notion_page(
cls : Type[T],
page_properties : Dict[str, Any],
page_parser : Optional[Callable] = None
) -> Optional[T]:
"""
Cria uma instância do schema a partir de propriedades parseadas do Notion.
Args:
page_properties: Dict com propriedades já parseadas pelo PropertyExtractor
page_parser: Função de parsing (se as propriedades ainda não foram parseadas)
Returns:
Instância do schema ou None se parsing falhar
"""
if not hasattr(cls, '_notion_config'):
raise AttributeError(
f"{cls.__name__} não tem NotionConfig definido. "
f"Adicione uma subclasse NotionConfig com os mappings."
)
config: NotionConfigMeta = cls._notion_config
# Se recebeu a página raw, faz o parsing
if page_parser and 'properties' in page_properties:
page_properties = page_parser(page_properties)
if not page_properties:
return None
result = {}
# Processa campos mapeados
for field_name, notion_name in config.field_mappings.items():
raw_value = page_properties.get(notion_name)
# Aplica transformer se existir
if field_name in config.transformers:
value = config.transformers[field_name](raw_value)
else:
value = raw_value
result[field_name] = value
# Cria instância temporária para computed fields
instance = cls(**result)
# Processa computed fields
for field_name, compute_func in config.computed.items():
computed_value = compute_func(instance)
result[field_name] = computed_value
# Retorna instância final com computed fields
return cls(**result)
@classmethod
def get_database_id(cls) -> str:
"Retorna o nome da database no Notion"
if hasattr(cls, '_notion_config'):
return cls._notion_config.database_id or cls.__name__
return cls.__name__
@classmethod
def get_notion_field_name(cls,
python_field : str
) -> Optional[str]:
"Retorna o nome do campo no Notion a partir do nome Python"
if hasattr(cls, '_notion_config'):
return cls._notion_config.field_mappings.get(python_field)
return None
@classmethod
def get_all_mappings(cls) -> Dict[str, str]:
"Retorna todos os mappings field_python -> field_notion"
if hasattr(cls, '_notion_config'):
return cls._notion_config.field_mappings.copy()
return {}
__all__ = ["NotionDatabase"]
+92
View File
@@ -0,0 +1,92 @@
from typing import Dict, Any, Optional, Callable, Type
from .database import NotionDatabase as _NotionDatabase
class DatabaseRegistry:
"""
Registry global de databases do Notion.
Permite acessar schemas e criar parsers dinamicamente.
"""
_databases: Dict[str, Type[_NotionDatabase]] = {}
@classmethod
def register(cls,
database_class : Type[_NotionDatabase]
) -> None:
"Registra uma database no registry"
db_name = database_class.get_database_id()
cls._databases[db_name] = database_class
@classmethod
def get(cls,
database_id : str
) -> Optional[Type[_NotionDatabase]]:
"Retorna a classe de schema de uma database"
return cls._databases.get(database_id)
@classmethod
def get_parser(cls,
database_id : str,
page_parser : Callable
) -> Callable:
"""
Retorna uma função parser para a database.
Args:
database_id: ID da Database
page_parser: Função que parseia páginas (ex: PageProperties.parse)
Returns:
Função parser ou None se database não encontrada
"""
db_class = cls.get(database_id)
if not db_class:
raise ValueError(f"Database de ID '{database_id}' não está registrada")
def parser(page : Dict[str, Any]) -> Optional[_NotionDatabase]:
return db_class.from_notion_page(page, page_parser)
return parser
@classmethod
def list_databases(cls) -> list[str]:
"Lista todas as databases registradas"
return list(cls._databases.keys())
@classmethod
def auto_register(cls,
*database_classes : Type[_NotionDatabase]
) -> None:
"Registra múltiplas databases de uma vez"
for db_class in database_classes:
cls.register(db_class)
@classmethod
def generate_literal_type(cls) -> str:
"Gera o código do Literal com todas as databases registradas"
db_names = list(cls._databases.keys())
literal_str = ", ".join([f'"{name}"' for name in db_names])
return f"Literal[{literal_str}]"
@classmethod
def get_database_class_by_id(cls,
db_id : str
) -> Optional[Type[_NotionDatabase]]:
"Retorna a classe schema baseado no nome da database"
return cls._databases.get(db_id)
+36
View File
@@ -0,0 +1,36 @@
from typing import Optional, Dict, Any
from urllib.parse import unquote
from ..extrators.Properties import PropertyExtractor as _PropertyExtractor
class _PageProperties(_PropertyExtractor):
"Parser completo de página do Notion. Retorna todas as propriedades parseadas de uma vez."
def parse(self, page: Dict[str, Any]) -> Optional[Dict[str, Any]]:
result = {}
if page.get("object") == "property_item":
value = self.extract(page)
if value is not None:
result[unquote(page["id"])] = value
return result
properties = page.get("properties")
if not properties:
return None
for prop_name, prop_data in properties.items():
value = self.extract(prop_data)
if value is not None:
result[prop_name] = value
return result
PageProperties = _PageProperties()
__all__ = ["PageProperties"]
+9
View File
@@ -0,0 +1,9 @@
from .PageProperties import PageProperties as _PageProperties
class _Parser:
def __init__(self) -> None:
self.page_props = _PageProperties.parse
Parser = _Parser()
__all__ = ["Parser"]
+26
View File
@@ -0,0 +1,26 @@
from typing import TypeVar, Generic, TYPE_CHECKING
if TYPE_CHECKING:
from ...schemas.orm.database.DatabasesContainer import DatabasesContainer as _DatabasesContainer
TContainer = TypeVar('TContainer', bound = '_DatabasesContainer')
class DatabasesRepo(Generic[TContainer]):
def __init__(self):
self.container: TContainer
@staticmethod
def generic(database_id : str):
from .databases import Database
return Database.generic(database_id)
class _Repositories(Generic[TContainer]):
def __init__(self):
from .pages import _Pages
self.pages = _Pages()
self.databases: DatabasesRepo[TContainer] = DatabasesRepo()
repo: _Repositories = _Repositories()
__all__ = ["repo", "_Repositories"]
+93
View File
@@ -0,0 +1,93 @@
from typing import Optional, Literal, Generic, TypeVar
from pydantic import validate_call
from ....schemas.responses import Schemas as _schm
from ...mapping.database import NotionDatabase as _NotionDatabase
from ...mapping import Mapping as _map
from ...parsers import Parser as _parser
from ...common.SetProperty import SetProperty as _setProperty
from ..pages.CreatePage import CreatePage as _CreatePage
TDB = TypeVar('TDB', bound = _NotionDatabase)
class CreateDatabasePage(Generic[TDB]):
def __init__(self,
database_id : str,
generic_response : bool = False
) -> None:
self._database_id = database_id
self._generic_response = generic_response
self._instance = _CreatePage().set_parent(
type = "database_id",
parent_id = self._database_id
)
self.set_property = _setProperty(self, self._instance.data["properties"])
@validate_call
def set_template(self,
type : Literal["default", "template_id"],
template_id : Optional[str] = None
):
self._instance.set_template(
type = type,
template_id = template_id
)
return self
@validate_call
def set_title(self,
prop_name : Optional[str],
prop_value : Optional[str]
):
self._instance.set_title(
prop_name = prop_name,
prop_value = prop_value
)
return self
@validate_call
def set_icon(self,
type : Literal["external"],
content : Optional[str] = None
):
self._instance.set_icon(
type = type,
content = content
)
return self
@validate_call
async def call(self,
map_properties : bool = True,
raw_response : bool = False
) -> _schm.pages.Page[TDB]:
page = await self._instance.call(
parse_properties = False
)
if not raw_response:
if map_properties:
parser = None
if not self._generic_response:
# Tenta pegar parser do registry
parser = _map.registry.get_parser(
database_id = self._database_id,
page_parser = _parser.page_props
)
# Fallback: se database não registrada, usa parser genérico
if not parser:
parser = _parser.page_props
page = page.model_dump()
mapped_properties = parser(page = page)
page["properties"] = mapped_properties
page = _schm.pages.Page(**page)
return page
__all__ = ["CreateDatabasePage"]
+101
View File
@@ -0,0 +1,101 @@
from typing import Optional, Generic, TypeVar
from pydantic import validate_call
from ....client import get_client as _get_client
from ....schemas.responses import Schemas as _schm
from ...mapping.database import NotionDatabase as _NotionDatabase
from ...mapping import Mapping as _map
from ...common.QueryFilter import QueryFilter as _Filter, _NotionFilter
from ...common.QuerySort import QuerySort as _Sort, _NotionSort
from ...parsers import Parser as _parser
TDB = TypeVar('TDB', bound = _NotionDatabase)
class SearchPage(Generic[TDB]):
def __init__(self,
database_id : str,
generic_response : bool = False
) -> None:
self._database_id = database_id
self._generic_response = generic_response
self.query_limit : int = 100
self.filter = _Filter
self._filter_obj : Optional[_NotionFilter] = None
self.sort = _Sort
self._sort_obj : Optional[_NotionSort] = None
def set_limit(self,
page_limit : int
):
"Define o limite de páginas a serem retornadas na requisição"
self.query_limit = page_limit
return self
def set_sort(self,
sort_obj : _NotionSort
):
"Define a classificação a ser usada na query"
self._sort_obj = sort_obj
return self
def set_filter(self,
filter_obj : _NotionFilter
):
"Define o filtro a ser usado na query"
self._filter_obj = filter_obj
return self
@validate_call
async def call(self,
map_properties : bool = True,
raw_response : bool = False
) -> _schm.databases.Query[TDB]:
payload = {}
if self.query_limit:
payload["page_size"] = self.query_limit
if self._sort_obj:
payload["sorts"] = self._sort_obj.to_dict()
if self._filter_obj:
payload["filter"] = self._filter_obj.to_dict()
client = _get_client()
query = await client.databases.query(
database_id = self._database_id,
json_data = payload
)
if query['object'] == 'error':
error = _schm.errors.Error(**query)
raise KeyError(error.__dict__)
pages = []
results = query["results"]
if not raw_response:
if map_properties:
parser = None
if not self._generic_response:
# Tenta pegar parser do registry
parser = _map.registry.get_parser(
database_id = self._database_id,
page_parser = _parser.page_props
)
# Fallback: se database não registrada, usa parser genérico
if not parser:
parser = _parser.page_props
for page in results:
page_properties = parser(page)
page["properties"] = page_properties if page_properties else None
pages.append(page)
results = pages
return _schm.databases.Query(**query)
__all__ = ["SearchPage"]
+94
View File
@@ -0,0 +1,94 @@
from typing import Optional, Literal, List
from ....client import get_client as _get_client
from ....schemas.orm.database import Schemas as _schm
from ....schemas.responses import Schemas as _schmResponses
from ...common.QueryFilter import QueryFilter as _Filter, _NotionFilter
from ...parsers import Parser as _parser
class SearchPageProperty:
"""
# CUIDADO AO UTILIZAR!!
### [Bug Conhecido](https://community.latenode.com/t/notion-api-relation-property-showing-empty-array-despite-ui-showing-connected-pages/25780) na API do Notion impede retornos confiáveis em propriedades paginadas
Ultimo teste : 2026-01-10
"""
def __init__(self, database_id : str) -> None:
self._database_id = database_id
self.filter = _Filter
self._filter_obj : Optional[_NotionFilter] = None
self._property_type = None
def set_filter(self,
property_type : Literal[
"unique_id",
"icon",
"title",
"number",
"checkbox",
"start_date",
"end_date",
#"relation",
"text",
"rich_text",
"select",
"multi_select",
"status",
"media",
"url",
"email",
"phone",
"person",
"place"
],
filter_obj : _NotionFilter
):
"Define o filtro a ser usado na query"
self._property_type = property_type
self._filter_obj = filter_obj
return self
async def call(self,
raw_response : bool = False
) -> List[_schm.SearchPageProperty]:
if not self._property_type:
raise TypeError("Filter is not fully defined")
payload = {}
if self._filter_obj:
payload["filter"] = self._filter_obj.to_dict()
client = _get_client()
query = await client.databases.query_propriety(
database_id = self._database_id,
propriety_type = self._property_type,
json_data = payload
)
if query['object'] == 'error':
error = _schmResponses.errors.Error(**query)
raise KeyError(error.__dict__)
results = query["results"]
pages = []
for page in results:
if raw_response:
page["properties"] = _parser.page_props(page = page)
pages.append(page)
result = []
for page in pages:
prop_value = page["properties"]
if prop_value:
prop_value = list(prop_value.values())[0]
result.append(
_schm.SearchPageProperty(
page_id = page["id"],
prop_value = prop_value
)
)
return result
+57
View File
@@ -0,0 +1,57 @@
from typing import Generic, TypeVar
from ...mapping.database import NotionDatabase as _NotionDatabase
from ..pages import _Pages as _Pages
from .CreateDatabasePage import CreateDatabasePage as _CreateDatabasePage
from .SearchPage import SearchPage as _SearchPage
from .SearchPageProperty import SearchPageProperty as _SearchPageProperty
TDB = TypeVar('TDB', bound = _NotionDatabase)
class Database(Generic[TDB]):
def __init__(self,
database_id : str,
generic_response : bool = False
) -> None:
self._database_id = database_id
self._generic_response = generic_response
@property
def CreateDatabasePage(self) -> _CreateDatabasePage[TDB]:
return _CreateDatabasePage(
database_id = self._database_id,
generic_response = self._generic_response
)
@property
def SearchPage(self) -> _SearchPage[TDB]:
return _SearchPage(
database_id = self._database_id,
generic_response = self._generic_response
)
@property
def SearchPageProperty(self) -> _SearchPageProperty:
return _SearchPageProperty(
database_id = self._database_id
)
@property
def page(self) -> _Pages[TDB]:
return _Pages(
database_id = self._database_id,
generic_response = self._generic_response
)
@staticmethod
def generic(database_id : str) -> 'Database[_NotionDatabase]':
"Acessa database sem schema definido"
return Database(
database_id = database_id,
generic_response = True
)
__all__ = ["Database"]
+8
View File
@@ -0,0 +1,8 @@
# async def main():
# instance = NotionOrm.repo.pages()
# create = await instance.CreatePage()\
# .set_parent("page_id", "2a564c9be67881a185c1c5d9133b9b1c")\
# .set_title("Name", "Teste abc")\
# .set_children("heading_1", "Teste")\
# .call()
# return create
+22
View File
@@ -0,0 +1,22 @@
import sys, os, asyncio
sys.path.append(
os.path.abspath(
os.path.join(
os.path.dirname(__file__), '..', '..', '..', '..', '..', '..', '..'
)
)
)
from src.utils.pprint import pprint
from src.integrations.notion.orm.repositories.pages.GetPage import GetPage
async def main():
instance = GetPage()
search = await instance\
.set_database(name="accounts")\
.set_pageid("0db2806f-b365-4327-919d-afbd1943f2ad")\
.select("Name")
#.call(True)
return search
test = await main()
pprint(test)
+21
View File
@@ -0,0 +1,21 @@
import sys, os, asyncio
sys.path.append(
os.path.abspath(
os.path.join(
os.path.dirname(__file__), '..', '..', '..', '..', '..', '..', '..'
)
)
)
from src.utils.pprint import pprint
from src.integrations.notion.orm.repositories.pages.GetPageProperty import GetPageProperty
async def main():
instance = GetPageProperty()
search = await instance\
.set_pageid("0db2806f-b365-4327-919d-afbd1943f2ad")\
.set_propname("Movements")\
.call()
return search
test = await main()
pprint(test)
View File
+176
View File
@@ -0,0 +1,176 @@
from typing import Literal, Any, Optional
from pydantic import validate_call
from ....schemas.responses.pages.Page import Page as _schmPage
from ....schemas.responses.errors.Error import Error as _schmError
from ....client import get_client as _get_client
from ...common.SetProperty import SetProperty as _setProperty
from ...parsers import Parser as _parser
class CreatePage:
def __init__(self) -> None:
self.data : dict[str, Any] = {
"properties": {}
}
self.set_property = _setProperty(self, self.data["properties"])
@validate_call
def set_parent(self,
type : Literal["page_id", "database_id"],
parent_id : str
):
self.data["parent"] = {
type : parent_id
}
return self
@validate_call
def set_template(self,
type : Literal["default", "template_id"],
template_id : Optional[str] = None
):
if type == "template_id":
if template_id is None:
raise TypeError("Template Type was set to 'template_id' but no ID was provided")
self.data["template"] = {
"type": type,
"template_id": template_id
}
return self
self.data["template"] = {
"type": type
}
return self
@validate_call
def set_title(self,
prop_name : Optional[str],
prop_value : Optional[str]
):
if prop_value is None:
return self
if self.data["parent"].get("page_id"):
self.data["properties"] = {
"title": [
{
"text": {
"content": prop_value
}
}
]
}
return self
self.data["properties"][prop_name] = {
"title": [
{
"text": {
"content": prop_value
}
}
]
}
return self
@validate_call
def set_icon(self,
type: Literal["external"],
content: Optional[str]
):
if content is None:
return self
match type:
case "external":
self.data["icon"] = {
"external": {
"url": str(content)
}
}
return self
@validate_call
def set_children(self,
type: Literal["heading_1", "paragraph"],
content: Any
):
if self.data.get("children") is None:
self.data["children"] = []
match type:
case "heading_1":
self.data["children"].append(
{
"object": "block",
"type": "heading_1",
"heading_1": {
"rich_text": [
{
"type": "text",
"text": {
"content": content
}
}
]
}
}
)
case "paragraph":
self.data["children"].append(
{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [
{
"type": "text",
"text": {
"content": content
}
}
]
}
}
)
return self
async def call(self,
parse_properties : bool = True
) -> _schmPage:
client = _get_client()
create = await client.pages.create(
json_data = self.data
)
if create['object'] == 'error':
error = _schmError(**create)
raise KeyError(error.__dict__)
if parse_properties:
properties = _parser.page_props(page = create)
create["properties"] = properties
return _schmPage(**create)
__all__ = ["CreatePage"]
+92
View File
@@ -0,0 +1,92 @@
from typing import Dict, Any, Optional, TypeVar, Generic
from ....schemas.responses.pages.Page import Page as _schmPage
from ....schemas.responses.errors.Error import Error as _schmError
from ....client import get_client as _get_client
from ...mapping.database import NotionDatabase as _NotionDatabase
from ...mapping import Mapping as _map
from ...parsers import Parser as _parser
TDB = TypeVar('TDB', bound =_NotionDatabase)
class GetPage(Generic[TDB]):
def __init__(self,
database_id : Optional[str] = None,
generic_response : bool = False
) -> None:
self._database_id : Optional[str] = database_id
self._generic_response = generic_response
self._pageid : str
def set_pageid(self,
id : str
) -> 'GetPage[TDB]':
self._pageid = id
return self
def set_database(self,
id : str
) -> 'GetPage[TDB]':
self._database_id = id
return self
async def select(self,
property : str
) -> Optional[Any]:
client = _get_client()
page : Dict[str, Any] = await client.pages.get(
page_id = self._pageid
)
if page['object'] == 'error':
error = _schmError(**page)
raise KeyError(error.__dict__)
page["properties"] = _parser.page_props(page = page)
if page["properties"] is None:
return None
allprops = page["properties"]
return allprops[property]
async def call(self,
map_properties : bool = True,
raw_response : bool = False
) -> _schmPage[TDB]:
client = _get_client()
page : Dict[str, Any] = await client.pages.get(
page_id = self._pageid
)
if page['object'] == 'error':
error = _schmError(**page)
raise KeyError(error.__dict__)
if not raw_response:
if map_properties and self._database_id:
parser = None
if not self._generic_response:
# Tenta pegar parser do registry
parser = _map.registry.get_parser(
database_id = self._database_id,
page_parser = _parser.page_props
)
# Fallback: se database não registrada, usa parser genérico
if not parser:
parser = _parser.page_props
mapped_properties = parser(page = page)
page["properties"] = mapped_properties
elif map_properties:
page["properties"] = _parser.page_props(page = page)
return _schmPage(**page)
+63
View File
@@ -0,0 +1,63 @@
from pydantic import validate_call
from typing import Dict, Any, Optional
from ....schemas.responses.errors.Error import Error as _schmError
from ....client import get_client as _get_client
from ...parsers.PageProperties import PageProperties as _PageProperties
class GetPageProperty:
"""
# CUIDADO AO UTILIZAR!!
### [Bug Conhecido](https://community.latenode.com/t/notion-api-relation-property-showing-empty-array-despite-ui-showing-connected-pages/25780) na API do Notion impede retornos confiáveis em propriedades paginadas
Ultimo teste : 2026-01-10
"""
def __init__(self) -> None:
self._pageid : str
self._propname : str
@validate_call
def set_pageid(self,
id : str
):
self._pageid = id
return self
@validate_call
def set_propname(self,
name : str
):
self._propname = name
return self
async def call(self,
raw_response : bool = False
) -> Optional[Dict[str, Any]]:
client = _get_client()
getprop = await client.pages.get_property(
page_id = self._pageid,
property_name = self._propname
)
if getprop['object'] == 'error':
error = _schmError(**getprop)
raise KeyError(error.__dict__)
prop_data = getprop.get("results")
if getprop.get("object") == "property_item":
prop_data = [getprop]
if not prop_data:
return None
if not raw_response:
result = _PageProperties.parse(
page = prop_data[0]
)
result = prop_data[0]
return result
+30
View File
@@ -0,0 +1,30 @@
from typing import Optional, Generic, TypeVar
from ...mapping.database import NotionDatabase as _NotionDatabase
from .CreatePage import CreatePage as _CreatePage
from .GetPage import GetPage as _GetPage
from .GetPageProperty import GetPageProperty as _GetPageProperty
TDB = TypeVar('TDB', bound = _NotionDatabase)
class _Pages(Generic[TDB]):
def __init__(self,
database_id : Optional[str] = None,
generic_response : bool = False
) -> None:
self._database_id = database_id
self._generic_response = generic_response
self.CreatePage = _CreatePage
self.GetPageProperty = _GetPageProperty
@property
def GetPage(self) -> _GetPage:
return _GetPage(
database_id = self._database_id,
generic_response = self._generic_response
)
Pages = _Pages()
__all__ = ["Pages", "_Pages"]
+9
View File
@@ -0,0 +1,9 @@
from .responses import Schemas as _responses
from .orm import Schemas as _orm
class Schemas:
responses = _responses
orm = _orm
__all__ = ["Schemas"]
+13
View File
@@ -0,0 +1,13 @@
from pydantic import BaseModel, ConfigDict
from datetime import datetime, date
class BaseModelSdk(BaseModel):
"Pydantic BaseModel com configuraçoes Padronizadas para o SDK"
model_config = ConfigDict(
json_encoders = {
datetime: lambda v: v.strftime("%Y-%m-%d %H:%M:%S"),
date: lambda v: v.strftime("%Y-%m-%d")
}
)
+7
View File
@@ -0,0 +1,7 @@
from .database import Schemas as _database
class Schemas:
database = _database
__all__ = ["Schemas"]
+3
View File
@@ -0,0 +1,3 @@
class DatabasesContainer:
pass
+8
View File
@@ -0,0 +1,8 @@
from ....schemas.dto import BaseModelSdk
from pydantic import ConfigDict
from typing import Any, Optional
class SearchPageProperty(BaseModelSdk):
model_config = ConfigDict(title="Notion_Orm_Database_SearchPageProperty")
page_id : str
prop_value : Optional[Any] = None
+9
View File
@@ -0,0 +1,9 @@
from .DatabasesContainer import DatabasesContainer as _DatabasesContainer
from .SearchPageProperty import SearchPageProperty as _SearchPageProperty
class Schemas:
DatabasesContainer = _DatabasesContainer
SearchPageProperty = _SearchPageProperty
__all__ = ["Schemas"]
+9
View File
@@ -0,0 +1,9 @@
from ....schemas.dto import BaseModelSdk
from pydantic import ConfigDict
from typing import Dict, Any, List, Optional
from ...responses.pages.properties.RichText import RichText as _RichText
class RichText(BaseModelSdk):
model_config = ConfigDict(title="Notion_Orm_Common_RichText")
text: Optional[str]
detailed: List[_RichText]
+7
View File
@@ -0,0 +1,7 @@
from .RichText import RichText as _RichText
class Schemas:
RichText = _RichText
__all__ = ["Schemas"]
+13
View File
@@ -0,0 +1,13 @@
from .databases import Schemas as _databases
from .pages import Schemas as _pages
from .users import Schemas as _users
from .errors import Schemas as _errors
class Schemas:
databases = _databases
pages = _pages
users = _users
errors = _errors
__all__ = ["Schemas"]
+15
View File
@@ -0,0 +1,15 @@
from ....schemas.dto import BaseModelSdk
from pydantic import ConfigDict
from typing import Optional, List, TypeVar, Generic
from ....orm.mapping.database import NotionDatabase as _NotionDatabase
from ..pages.Page import Page as _Page
TDB = TypeVar('TDB', bound = _NotionDatabase)
class Query(BaseModelSdk, Generic[TDB]):
model_config = ConfigDict(title="Notion_Responses_Databases_Query")
results: List[_Page[TDB]]
next_cursor: Optional[str] = None
has_more: bool = False
type: str = "page_or_database"
page_or_database: dict = {}
+7
View File
@@ -0,0 +1,7 @@
from .Query import Query as _Query
class Schemas:
Query = _Query
__all__ = ["Schemas"]
+8
View File
@@ -0,0 +1,8 @@
from ....schemas.dto import BaseModelSdk
from pydantic import ConfigDict
class Error(BaseModelSdk):
model_config = ConfigDict(title="Notion_Responses_Errors_Error")
status: int
code: str
message: str
+7
View File
@@ -0,0 +1,7 @@
from .Error import Error as _Error
class Schemas:
Error = _Error
__all__ = ["Schemas"]
+24
View File
@@ -0,0 +1,24 @@
from ....schemas.dto import BaseModelSdk
from pydantic import ConfigDict
from typing import Optional, Any, Dict, Generic, TypeVar, Union
from datetime import datetime
from ....orm.mapping.database import NotionDatabase as _NotionDatabase
from ..users.User import User as _User
from .Parent import Parent as _Parent
TDB = TypeVar('TDB', bound = _NotionDatabase)
class Page(BaseModelSdk, Generic[TDB]):
model_config = ConfigDict(title="Notion_Responses_Pages_Page")
id: str
created_time: datetime
last_edited_time: datetime
created_by: _User
last_edited_by: _User
cover: Optional[Dict[str, Any]]
icon: Optional[Dict[str, Any]]
parent: _Parent
archived: bool
properties: Union[Any, TDB]
url: str
public_url: Optional[str]
+9
View File
@@ -0,0 +1,9 @@
from ....schemas.dto import BaseModelSdk
from pydantic import ConfigDict
from typing import Optional, Literal
class Parent(BaseModelSdk):
model_config = ConfigDict(title="Notion_Responses_Pages_Parent")
type: Literal ["page_id", "data_source_id", "database_id"]
data_source_id: Optional[str] = None
database_id: Optional[str] = None
+9
View File
@@ -0,0 +1,9 @@
from .Page import Page as _Page
from .Parent import Parent as _Parent
class Schemas:
Page = _Page
Parent = _Parent
__all__ = ["Schemas"]
+23
View File
@@ -0,0 +1,23 @@
from .....schemas.dto import BaseModelSdk
from pydantic import ConfigDict
from typing import Dict, Any, Optional
class RichText(BaseModelSdk):
model_config = ConfigDict(title="Notion_Responses_Pages_Properties_RichText")
type: str
text: 'Text'
annotations: 'Annotations'
plain_text: Optional[str]
href: Optional[str]
class Text(BaseModelSdk):
content: Optional[str]
link: Optional[str]
class Annotations(BaseModelSdk):
bold: bool
italic: bool
strikethrough: bool
underline: bool
code: bool
color: str
+7
View File
@@ -0,0 +1,7 @@
from .RichText import RichText as _RichText
class Schemas:
RichText = _RichText
__all__ = ["Schemas"]
+11
View File
@@ -0,0 +1,11 @@
from ....schemas.dto import BaseModelSdk
from pydantic import ConfigDict
from .type.Bot import Bot as _Bot
class Bot(BaseModelSdk):
model_config = ConfigDict(title="Notion_Responses_Users_Bot")
id: str
type: str = "bot"
bot: _Bot
name: str
avatar_url: str
+11
View File
@@ -0,0 +1,11 @@
from ....schemas.dto import BaseModelSdk
from pydantic import ConfigDict
from .type.Person import Person as _Person
class Person(BaseModelSdk):
model_config = ConfigDict(title="Notion_Responses_Users_Person")
id: str
type: str = "person"
person: _Person
name: str
avatar_url: str
+6
View File
@@ -0,0 +1,6 @@
from ....schemas.dto import BaseModelSdk
from pydantic import ConfigDict
class User(BaseModelSdk):
model_config = ConfigDict(title="Notion_Responses_Users_User")
id: str
+13
View File
@@ -0,0 +1,13 @@
from .type import Schemas as _type
from .User import User as _User
from .Person import Person as _Person
from .Bot import Bot as _Bot
class Schemas:
type = _type
User = _User
Person = _Person
Bot = _Bot
__all__ = ["Schemas"]
+6
View File
@@ -0,0 +1,6 @@
from .....schemas.dto import BaseModelSdk
from pydantic import ConfigDict
class Bot(BaseModelSdk):
model_config = ConfigDict(title="Notion_Responses_Users_Type_Bot")
...
+6
View File
@@ -0,0 +1,6 @@
from .....schemas.dto import BaseModelSdk
from pydantic import ConfigDict
class Person(BaseModelSdk):
model_config = ConfigDict(title="Notion_Responses_Users_Type_Person")
email: str
+9
View File
@@ -0,0 +1,9 @@
from .Person import Person as _Person
from .Bot import Bot as _Bot
class Schemas:
Person = _Person
Bot = _Bot
__all__ = ["Schemas"]
View File
+3
View File
@@ -0,0 +1,3 @@
from ...orm.mapping.database import NotionDatabase
from ...orm.mapping.registry import DatabaseRegistry
from ...schemas.orm.database.DatabasesContainer import DatabasesContainer
+1
View File
@@ -0,0 +1 @@
from ....schemas.orm.properties.RichText import RichText
View File
+1
View File
@@ -0,0 +1 @@
from ....schemas.responses.databases.Query import Query
+1
View File
@@ -0,0 +1 @@
from ....schemas.responses.errors.Error import Error
+2
View File
@@ -0,0 +1,2 @@
from ....schemas.responses.pages.Page import Page
from ....schemas.responses.pages.Parent import Parent
+3
View File
@@ -0,0 +1,3 @@
from ....schemas.responses.users.Bot import Bot
from ....schemas.responses.users.Person import Person
from ....schemas.responses.users.User import User
Executable
+21
View File
@@ -0,0 +1,21 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "sdk-notion"
version = "0.1.0"
description = "Async Python SDK for the Notion API"
readme = "README.md"
requires-python = ">=3.9"
license = { text = "MIT" }
authors = [
{ name = "Eduardo Riguetto" }
]
dependencies = [
"httpx>=0.25,<1.0",
"pydantic>=2.5,<3.0"
]
[project.urls]
Homepage = "https://github.com/riguettodev/sdk-notion"