A high-level, typed, retry-aware Python client for Notion workspaces.
Project description
notion-ops
A high-level Python library for CRUD operations on Notion workspaces.
Installation
pip install notion-ops
Quick Start
from notion_ops import NotionOps, Blocks, Filter, Sort
from notion_ops.models import TitleProperty, SelectProperty, CheckboxProperty
# Initialize client (uses NOTION_API_KEY env var)
client = NotionOps()
# Create a page
page = client.pages.create(
parent_id="database_id",
properties={
"Name": TitleProperty(value="New Task"),
"Status": SelectProperty(value="In Progress"),
},
children=[
Blocks.heading_1("Overview"),
Blocks.paragraph("Task description here."),
]
)
# Query a database
results = client.data_sources.query(
"database_id",
filter=Filter.and_(
Filter.select("Status").equals("Active"),
Filter.checkbox("Archived").equals(False)
),
sorts=[Sort.descending("Created")]
)
for page in results.pages:
print(page.get_title())
Features
- Full CRUD Operations: Create, Read, Update, Delete for pages, databases, and blocks
- Type-Safe Models: Pydantic models for all Notion objects
- Query Builders: Fluent API for building filters and sorts
- Block Builders: Easy creation of all Notion block types
- Pagination Helpers: Automatic handling of paginated results
- Rich Text Utilities: Convert between plain text, rich text, and markdown
- Limit-Aware Publishing: Publish Markdown or arbitrarily-nested block trees in the minimum number of requests, automatically respecting Notion's 2-level inline-nesting cap, the 100-children-per-request limit, >100-row table splitting, and payload-size limits — no manual batching
API Reference
Client
from notion_ops import NotionOps
# Initialize with environment variable
client = NotionOps()
# Or with explicit token
client = NotionOps(auth="secret_xxx")
# Access operations
client.pages # Page CRUD
client.databases # Database CRUD
client.data_sources # Data source queries
client.blocks # Block CRUD
client.users # User operations
Pages
# Create
page = client.pages.create(
parent_id="db_id",
properties={"Name": TitleProperty(value="Title")},
)
# Read
page = client.pages.get("page_id")
print(page.get_title())
print(page.get_property("Status"))
# Update
page = client.pages.update(
"page_id",
properties={"Status": SelectProperty(value="Done")}
)
# Archive/Delete
client.pages.archive("page_id")
Databases & Data Sources
# Create database
db = client.databases.create(
parent_id="page_id",
title="Tasks",
schema={
"Name": PropertyDefinition(name="Name", type=PropertyType.TITLE),
"Status": PropertyDefinition(
name="Status",
type=PropertyType.STATUS,
options={"options": [{"name": "Done", "color": "green"}]}
),
}
)
# Query with filters
results = client.data_sources.query(
"data_source_id",
filter=Filter.select("Status").equals("Active"),
sorts=[Sort.descending("Created")]
)
# Iterate all pages
for page in client.data_sources.query_all("data_source_id"):
print(page.get_title())
Blocks
# Get page content
blocks = client.blocks.get_children(page_id, recursive=True)
# Append content
client.blocks.append(
page_id,
[
Blocks.heading_2("Section"),
Blocks.paragraph("Content here."),
Blocks.bulleted_list("Item 1"),
Blocks.code("print('hello')", language="python"),
Blocks.callout("Note!", emoji="💡"),
]
)
# Update block
client.blocks.update(block_id, content={"rich_text": [...]})
# Delete block
client.blocks.delete(block_id)
Filters
from notion_ops import Filter
# Simple filters
Filter.title("Name").contains("test")
Filter.select("Status").equals("Active")
Filter.checkbox("Done").equals(True)
Filter.number("Count").greater_than(10)
Filter.date("Due").before(datetime.now())
# Compound filters
Filter.and_(
Filter.select("Status").equals("Active"),
Filter.checkbox("Archived").equals(False)
)
Filter.or_(
Filter.select("Status").equals("Done"),
Filter.select("Status").equals("Cancelled")
)
Block Builders
from notion_ops import Blocks
Blocks.paragraph("Text content")
Blocks.heading_1("Title")
Blocks.heading_2("Subtitle")
Blocks.heading_3("Section")
Blocks.bulleted_list("Item")
Blocks.numbered_list("Step")
Blocks.todo("Task", checked=False)
Blocks.toggle("Expandable")
Blocks.code("code", language="python")
Blocks.quote("Quote text")
Blocks.callout("Note", emoji="💡")
Blocks.divider()
Blocks.image("https://...")
Blocks.bookmark("https://...")
Publishing Markdown & nested content
blocks.children.append accepts at most two levels of nesting and 100 children
per request, and large tables or deeply-nested content must be split across
follow-up requests. publish_block_tree / publish_markdown plan and execute
the minimum sequence of appends that respects every one of those limits for you.
from notion_ops import NotionOps, publish_markdown, publish_block_tree, markdown_to_blocks
client = NotionOps()
# Publish a Markdown document (tables, nested lists, code, toggles, ...) under a page.
result = publish_markdown(client, "page_id", "# Report\n\n| a | b |\n| - | - |\n...")
print(result.request_count, result.top_level_block_ids)
# Or publish an already-built nested block tree.
blocks = markdown_to_blocks(some_markdown)
publish_block_tree(client, "page_id", blocks)
# Idempotent republish: re-running refreshes the page instead of duplicating it.
# Clears the existing top-level children, then publishes the new tree — exactly
# what you want when re-publishing a report/atom. Content is idempotent; block
# ids are not stable (cleared blocks are archived + recreated), and the clear +
# publish are not a single transaction.
from notion_ops import republish_markdown
result = republish_markdown(client, "page_id", "# Report (v2)\n\nupdated body")
print(result.deleted_count, result.request_count)
PageTemplate publishes its body through the same path, so templated pages get
the same limit-handling automatically.
Property Types
from notion_ops.models import (
TitleProperty,
RichTextProperty,
NumberProperty,
SelectProperty,
MultiSelectProperty,
DateProperty,
CheckboxProperty,
URLProperty,
EmailProperty,
PeopleProperty,
RelationProperty,
StatusProperty,
)
# Usage
properties = {
"Name": TitleProperty(value="Task Name"),
"Description": RichTextProperty(value="Details..."),
"Priority": NumberProperty(value=1),
"Status": SelectProperty(value="In Progress"),
"Tags": MultiSelectProperty(value=["urgent", "feature"]),
"Due Date": DateProperty(value=datetime(2025, 2, 1)),
"Completed": CheckboxProperty(value=False),
"Website": URLProperty(value="https://example.com"),
"Email": EmailProperty(value="user@example.com"),
"Assignees": PeopleProperty(value=["user_id_1"]),
"Related": RelationProperty(value=["page_id_1"]),
}
Development
# Install dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Type checking
mypy notion_ops
# Linting
ruff check notion_ops
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file notion_ops-0.1.0.tar.gz.
File metadata
- Download URL: notion_ops-0.1.0.tar.gz
- Upload date:
- Size: 117.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd1c3e204e93e8c20098a87ad40ae9399553080c45a0682e9f180110cdd96f2a
|
|
| MD5 |
12b778edf02ee9f2314aa9e899d12b63
|
|
| BLAKE2b-256 |
70ceedfbafe2e8e31266873a8383168b85b9b235ce2706304940d79fea479abc
|
Provenance
The following attestation bundles were made for notion_ops-0.1.0.tar.gz:
Publisher:
ci.yml on stephenBlackW/Notion-Ops
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
notion_ops-0.1.0.tar.gz -
Subject digest:
bd1c3e204e93e8c20098a87ad40ae9399553080c45a0682e9f180110cdd96f2a - Sigstore transparency entry: 1724305750
- Sigstore integration time:
-
Permalink:
stephenBlackW/Notion-Ops@74e4f86145b87cf750e8acc341710c2c5b2c9f12 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/stephenBlackW
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@74e4f86145b87cf750e8acc341710c2c5b2c9f12 -
Trigger Event:
push
-
Statement type:
File details
Details for the file notion_ops-0.1.0-py3-none-any.whl.
File metadata
- Download URL: notion_ops-0.1.0-py3-none-any.whl
- Upload date:
- Size: 60.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9cdab45abbd9e9ce00b6206a248fbfdb8b7ee4145ea88ec5de88b58c4fbdff20
|
|
| MD5 |
02b94daf02176637be5752a46be731ef
|
|
| BLAKE2b-256 |
dbabaa8fa152764032d8dcba925c809b881574999f3c7aa20f14ef311f6b6225
|
Provenance
The following attestation bundles were made for notion_ops-0.1.0-py3-none-any.whl:
Publisher:
ci.yml on stephenBlackW/Notion-Ops
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
notion_ops-0.1.0-py3-none-any.whl -
Subject digest:
9cdab45abbd9e9ce00b6206a248fbfdb8b7ee4145ea88ec5de88b58c4fbdff20 - Sigstore transparency entry: 1724305867
- Sigstore integration time:
-
Permalink:
stephenBlackW/Notion-Ops@74e4f86145b87cf750e8acc341710c2c5b2c9f12 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/stephenBlackW
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@74e4f86145b87cf750e8acc341710c2c5b2c9f12 -
Trigger Event:
push
-
Statement type: