Package: notion-api-py
notion-api-pyA wrapper aroundNotion Apiallowing you to create objects for your databases/pages and easily perform add/update/delete/filter operations in a more readable wayThere are many libraries,official/unofficialout there. However the motivation behind creating this is to have a more simplistic approach to the most basic operations to perform over aNotion databaseandNotion pages. Also, allowing forfiltering, handling pagination amoung query results and **Applying filters on a database view on Notion UI via code (Yes, the coolest unofficial thing so far 🥳) Refer sectionCollection view filterfor more details.Prerequiste: Generate Notion Api token and integrationBefore using this Api, you will need a API token from NotionPlease follow the below guide to know how to generate a api token for your Notion account, as well as how to sharedatabasesandpageswith your created integrationhttps://www.codingwithmiszu.com/2021/12/28/how-to-generate-a-notion-api-token-easily/https://daily-dev-tips.com/posts/getting-started-with-the-notion-api/To know more about NotionApi from the official sourceshttps://www.notion.so/help/create-integrations-with-the-notion-apihttps://developers.notion.com/docs/authorizationUsageVia the ‘Releases’ section of this repository to get the latest and greatest version of the libraryViapippython3-mpipinstallnotion-api-pyRECOMMENDED - Setting up thesecrets_file.pyIts recommened that all sensitive data be added to asecrets_file.pyfile, including the token, version, database ids and cookie related informationand not committed to the repository(add to.gitignore)# secrets_file.pytoken="secret_XXXXXXXXX"version="2021-08-16"database1_id="AAAAAAAAAAAAAAAAAAAAA"database2_id="BBBBBBBBBBBBBBBBBBBB"# used for notion Collection view filter filers - NOT OFFICIAL APInotion_client_version=''x_notion_active_user_header=''cookie='__cookie_string__'Notion DatabaseA wrapper class containing basic operations likeadd_page,update_page,delete_pageandfilteraround a basic Notion database.Example usage:To create an object ofTasksdatabase (example), all you need to do is relay the database_id asdatabase_id='XXXXXXX-XXXX-XXXX-XXXXXXXXX'ORdatabase_id=secrets_file.master_task_database_idto the extendedNotionDatabaseclass. The otheradd,update,deletecan simply be relayed to parentNotionDatabaseclass and it will take care of it.classTasks(NotionDatabase):def__init__(self):self.log=logging.getLogger(self.__class__.__name__)NotionDatabase.__init__(self,token=secrets_file.token,version=secrets_file.version,database_id=secrets_file.master_task_database_id)defadd(self,icon=None,properties=None):self.log.info("--------- Preparing to Add tasks ---------")returnself.add_page(icon=icon,properties=properties)defupdate(self,task_id=None,icon=None,properties=None):self.log.info("--------- Preparing to Update tasks ---------")returnself.update_page(page_id=task_id,icon=icon,properties=properties)defdelete(self,task_id):returnself.delete_page(page_id=task_id)deffilter(self,filter):returnsuper().filter(filter)Notion FiltersTheNotionFiltersclass provides a range of filters you can apply to the databases.Example usageincomplete_task_filter=NotionFilter(NotionFilterAnd(NotionRelationFilter("Release").contains("My Custom Release"),NotionTextFilter("Name").contains("Test Name"),NotionCheckboxFilter("✅ ?").does_not_equal(True)).build()).build()filtered_incomplete_tasks=Tasks().filter(incomplete_feature_filter)NotionFilterThis is wrapper/base that encapsulates all Notion filters. You can either give it a simple single filter (any one of the Single Filter,listed below) or a compund filterCompund filtersNotionFilterAndNotionFilterOrThese filter encapsulates the Single Filters listed belowSingle FiltersNotionRelationFilter- apply filter onRelationsfieldsNotionCheckboxFilter- apply filter onCheckboxfieldsNotionTextFilter- apply filter onTextfieldsNotionDateFilter- apply filter onDatefieldsNotionSelectFilter- apply filter onSelectfieldsNotionMultiSelecFilter- apply filter onMultiSelectfieldsNotionNumberFilterapply filter onNumberfieldsNotionFormulaFilter- apply filter onFormulafields. While dealing withNumber, Text, Date, CheckboxwithinNotionFormulaFilteruse theNotionFormulaNumberFilterNotionFormulaTextFilterNotionFormulaDateFilterNotionFormulaCheckboxFilterNotionFilesFilter- apply filter onFilesfieldsNotionPeopleFilter- apply filter onPeoplefieldsNotion PageA wrapper class containing basic operations likeadd_page,update_page,build_propertiesaround a basicNotion page.Example Usage:To represent aTaskspage in the aboveTasksdatabase, we can create aTasksPageobject extending the librariesNotionPageclass. By virtue, theTasksPageclass now inherits all basic operations -add_page,update_page,build_propertiesfromNotionPagereducing the code.classTasksPage(NotionPage):def__init__(self,page_id=None,existing_properties=None):NotionPage.__init__(self,token=secrets_file.token,version=secrets_file.version,page_id=page_id,properties=existing_properties)By default,page_idandexisting_propertiesare None. However, we need either thepage_idorexisting_properties.  When we are performing a filtering operation, the Notions filter results have all the information including the properties of the pages that come in the filtered result. In this case, we can simply pass the properties from the filtered results into the object while instantiatingThishelps reduce the number of api callsto Notion to fetch properties details later# Exampleincomplete_task_filter=NotionFilter(NotionFilterAnd(NotionRelationFilter("Release").contains("My Custom Release"),NotionTextFilter("Name").contains("Test Name"),NotionCheckboxFilter("✅ ?").does_not_equal(True)).build()).build()filtered_incomplete_tasks=Tasks().filter(incomplete_feature_filter)foreach_taskinfiltered_incomplete_tasks:# print("each_task", each_task)each_incomplete_task=TasksPage(existing_properties=each_task)#The properties of each_incomplete_task can then be retrieved by belowlogging.debug("Incomplete task ->%swith status%s",each_incomplete_task.get_property("Name"),each_incomplete_task.get_property("Status"))However, if we dont have properties, thenpage_idis mandatory as a api request would be made to fetch the page propertiesexisting_task=TasksPage(page_id="87ec7b33-7932-bg35-d7fa7580cc63")Dynamic Notion Request builderThe build_properties function is unique automatic dynamic request builder which auto creates the json required for update pageexisting_task=TasksPage(page_id="87ec7b33-7932-bg35-d7fa7580cc63")planned_days_relation=Relations().create("e9ec7b33-7932-bg35-d7fa7580cc63").append_to_existing(existing_task.get_property("Planned Day"))planned_week_relation=Relations().create("303d3e79-9000-3000-b942-34ffd349af75").append_to_existing(existing_task.get_property("Planned Week"))planned_month_relation=Relations().create("7896ty6r-344e-4763-819a-b7c7c405e4f0").append_to_existing(existing_task.get_property("Planned Month"))# The 'Tasks' page might have several properties, but we are interesting only in updating the "Planned Day","Planned Week","Planned Month"existing_task.update_page(icon="🔺",properties={"Planned Day":planned_days_relation,"Planned Week":planned_week_relation,"Planned Month":planned_month_relation})would translate to aRequestBodyinjsonsomething like{"icon":{"type":"emoji","emoji":"🔺"},"archived":false,"properties":{"Planned Day":{"type":"relation","relation":[{"id":"e9ec7b33-7932-bg35-d7fa7580cc63"}]},"Planned Week":{"type":"relation","relation":[{"id":"303d3e79-9000-3000-b942-34ffd349af75"}]},"Planned Month":{"type":"relation","relation":[{"id":"7896ty6r-344e-4763-819a-b7c7c405e4f0"}]}}}Another example withdatefields (which is slightly different)Tasks().add(icon="✅",properties={"Name":task_title,"Task":task_relation,"Hours spent":distributed_hours,"Journal Date":Relations().create(journal_date_id).overwrite(),"Time (Date)":{"start":"2021-12-31","end":None,"time_zone":None}})Notion PropertiesThis class is responsible for interpreting thepropertiesobject for anypageordatabasefrom the results of Notion requests.Its used implicitly within theNotionDatabaseandNotionPageclassesIts basically used to either read a specific property value from a json result or marshall a set of properties into a json request bodyNotion relationsThis class is responsible for handlingrelationsin databases, which is the basis of creating relation (much like foreign key) between different databases.#overwrites an existing relation with the new_relationRelations().create(new_relation).overwrite()#appends the new_relation to an existing list of relationsRelations().create(new_relation).append_to_existing(existing_relation)Practical usage exampleSay, extending the example above, if we useoverwrite()#Before{"icon":{"type":"emoji","emoji":"🔺"},"archived":false,"properties":{"Planned Day":{"type":"relation","relation":[{"id":"e9ec7b33-7932-bg35-d7fa7580cc63"}]},}}# Updating the Planned day relation with overwriteexisting_task=TasksPage(page_id="87ec7b33-7932-bg35-d7fa7580cc63")planned_days_relation=Relations().create("5555555-7932-bg35-d7fa7580cc63").overwrite()existing_task.update_page(icon="🔺",properties={"Planned Day":planned_days_relation,})#After update{"icon":{"type":"emoji","emoji":"🔺"},"archived":false,"properties":{"Planned Day":{"type":"relation","relation":[{"id":"5555555-7932-bg35-d7fa7580cc63"}]},}}With theappend_to_existingoption, it would add to existing relation, if already present#Before{"icon":{"type":"emoji","emoji":"🔺"},"archived":false,"properties":{"Planned Day":{"type":"relation","relation":[{"id":"e9ec7b33-7932-bg35-d7fa7580cc63"}]},}}# Updating the Planned day relation with overwriteexisting_task=TasksPage(page_id="87ec7b33-7932-bg35-d7fa7580cc63")planned_days_relation=Relations().create("5555555-7932-bg35-d7fa7580cc63").append_to_existing(existing_task.get_property("Planned Day"))existing_task.update_page(icon="🔺",properties={"Planned Day":planned_days_relation,})#After update{"icon":{"type":"emoji","emoji":"🔺"},"archived":false,"properties":{"Planned Day":{"type":"relation","relation":[{"id":"e9ec7b33-7932-bg35-d7fa7580cc63"},{"id":"5555555-7932-bg35-d7fa7580cc63"}]},}}More examples, can anyways seen in the code example of previous sections.Notion sortsNot available at the moment. This is on the wishlistCollection view filterThis is an exciting option, which allows you auto set filters on particularviewin Notion DatabasesTo know more about Notion database view, referhttps://www.notion.so/help/guides/using-database-viewsWhile the official Notion Api provides an option toquerydatabases -https://developers.notion.com/reference/post-database-queryit doesnt provide an ability to actually“set”the filters on the UI.Example use casesUse case 1:where I needed this. I havePlanned Days,Planned MonthsandPlanned Weeksas relation pages and I want that every morning, the filter automatically changes to reflect the correct date/week/month so I can plan my tasks accordinglyUse case 2:I use Notion for project management where I create Project → Features → Tasks. I have templates for each of these. So whenever I create a new feature using the template, it brings in the linked task database with theRelated Featuresfilter applied. However, I would also like “auto” apply theTags,ProjectsandReleaserelations, as I know them based on the Feature page we are on. As of this writing, Notion provides no way of doing this.I reverse engineered the api calls from Notion to thier server to see what they are doing when  we apply filter operations to a viewBased on my experiments, I designed wrapper classes around itPlease note this is unofficial api call, hence you will need a logged in session cookie to make this to workThere are ‘n’ number of tutorials/chrome extensions on the net, how to get the stored cookies for a site.Currenly this is not covered here.Once, you have the cookie details, we could use something like the below#Applying filters on UI for Use case 1query2filter=NotionWebQuery2(notion_web_filter=NotionWebQuery2Filter("and",NotionWebDbSimpleFilter("exact",day_page_id,"relation_contains",task_property_id_map.get("Planned Day")),NotionWebDbSimpleFilter("exact",week_page_id,"relation_contains",task_property_id_map.get("Planned Week")),NotionWebDbSimpleFilter("exact",month_page_id,"relation_contains",task_property_id_map.get("Planned Month")))#Optional, only if you require to provide aggregate operations on columns# In my use case, wanted to provide aggregate on time spent for tasks as well as number of tasks,notion_web_aggregations=NotionWebDbAggregations(NotionWebDbAggregation(task_property_id_map.get("Actual time spent")).sum(),NotionWebDbAggregation(task_property_id_map.get("Name")).count())).generate()Above, will generate thejsonRequestBody for the request to be sentFurther, you pass this filter togenerate_collection_view_filter_bodyfollowed bysend_collection_view_filter_request# the collection_id and collection_view_id can be fetched from url of the view# if you right click on a database view and use the "Copy link" you should get a url as# https://www.notion.so/XXXXXXXXXXX?v=YYYYYYYYY# Here the collection_id=XXXXXXXXXXX and collection_view_id="YYYYYYYYY"payload=generate_collection_view_filter_body(collection_id,collection_view_id,int(datetime.now().timestamp()),query2filter)# notion_client_version, x_notion_active_user_header, cookie# These details can be fetched by inspecting the Chrome devtools(Network) and the cookiesend_collection_view_filter_request("https://www.notion.so/api/v3/saveTransactions",payload,secrets_file.notion_client_version,secrets_file.x_notion_active_user_header,secrets_file.cookie)