コード例 #1
0
class CollectionView(Record):
    """
    A "view" is a particular visualization of a collection,
    with a "type" (board, table, list, etc) and filters, sort, etc.
    """

    _type = "collection_view"
    _table = "collection_view"

    name = field_map("name")
    type = field_map("type")

    def __init__(self, *args, collection, **kwargs):
        super().__init__(*args, **kwargs)
        self.collection = collection

    def build_query(self, **kwargs) -> CollectionQuery:
        return CollectionQuery(collection=self.collection,
                               collection_view=self,
                               **kwargs)

    def default_query(self) -> CollectionQuery:
        """
        Return default query.
        """
        return self.build_query(**self.get("query", {}))

    @property
    def parent(self):
        return self._client.get_block(self.get("parent_id"))
コード例 #2
0
ファイル: embed.py プロジェクト: Joss/notion-py
class BookmarkBlock(EmbedBlock):
    """
    Bookmark Block.
    """

    _type = "bookmark"

    bookmark_cover = field_map("format.bookmark_cover")
    bookmark_icon = field_map("format.bookmark_icon")
    description = property_map("description")
    link = property_map("link")
    title = property_map("title")

    def set_new_link(self, link: str):
        data = {"blockId": self.id, "url": link}
        self._client.post("setBookmarkMetadata", data)
        self.refresh()
コード例 #3
0
class ColumnBlock(Block):
    """
    Should be added as children of a ColumnListBlock.
    """

    _type = "column"

    column_ratio = field_map("format.column_ratio")
コード例 #4
0
class BasicBlock(Block):

    _type = "block"
    _str_fields = "title"

    title = property_map("title")
    title_plaintext = plaintext_property_map("title")
    color = field_map("format.block_color")
コード例 #5
0
ファイル: upload.py プロジェクト: Joss/notion-py
class UploadBlock(EmbedBlock):
    """
    Upload Block.
    """

    file_id = field_map(["file_ids", 0])

    def upload_file(self, path: str):
        """
        Upload a file and embed it in Notion.


        Arguments
        ---------
        path : str
            Valid path to a file.


        Raises
        ------
        HTTPError
            On API error.
        """

        content_type = guess_type(path)[0] or "text/plain"
        file_name = os.path.split(path)[-1]
        file_size = human_size(path)

        data = {
            "bucket": "secure",
            "name": file_name,
            "contentType": content_type
        }
        resp = self._client.post("getUploadFileUrl", data).json()

        with open(path, mode="rb") as f:
            response = requests.put(resp["signedPutUrl"],
                                    data=f,
                                    headers={"Content-Type": content_type})
            response.raise_for_status()

        query = urlencode({
            "cache": "v2",
            "name": file_name,
            "id": self._id,
            "table": self._table,
            "userId": self._client.current_user.id,
        })
        url = resp["url"]
        query_url = f"{url}?{query}"

        # special case for FileBlock
        if hasattr(self, "size"):
            setattr(self, "size", file_size)

        self.source = query_url
        self.display_source = query_url
        self.file_id = urlparse(url).path.split("/")[2]
コード例 #6
0
class EmbedBlock(MediaBlock):
    """
    Embed Block.
    """

    _type = "embed"
    _str_fields = "source"

    display_source = prefixed_field_map("format.display_source")
    source = prefixed_property_map("source")
    height = field_map("format.block_height")
    width = field_map("format.block_width")
    full_width = field_map("format.block_full_width")
    page_width = field_map("format.block_page_width")

    def set_source_url(self, url: str):
        self.source = remove_signed_prefix_as_needed(url)
        self.display_source = get_embed_link(self.source, self._client)
コード例 #7
0
ファイル: embed.py プロジェクト: Joss/notion-py
class EmbedBlock(MediaBlock):
    """
    Embed Block.
    """

    _type = "embed"
    _str_fields = "source"

    # TODO: why this exists? is it the same as `source`?
    display_source = prefixed_field_map("format.display_source")
    source = prefixed_property_map("source")
    height = field_map("format.block_height")
    width = field_map("format.block_width")
    full_width = field_map("format.block_full_width")
    page_width = field_map("format.block_page_width")

    def set_source_url(self, url):
        self.source = remove_signed_prefix_as_needed(url)
        self.display_source = get_embed_link(self.source)
コード例 #8
0
ファイル: user.py プロジェクト: Joss/notion-py
class NotionUser(Record):
    """
    Representation of a Notion user.
    """

    _table = "notion_user"
    _str_fields = "email", "full_name"

    user_id = field_map("user_id")
    given_name = field_map("given_name")
    family_name = field_map("family_name")
    email = field_map("email")
    locale = field_map("locale")
    time_zone = field_map("time_zone")

    @property
    def full_name(self):
        """
        Get full user name.


        Returns
        -------
        str
            User name.
        """
        given = self.given_name or ""
        family = self.family_name or ""
        return f"{given} {family}".strip()
コード例 #9
0
class BoardView(CollectionView):

    _type = "board"

    group_by = field_map("query.group_by")
コード例 #10
0
ファイル: space.py プロジェクト: huksley/notion-py
class NotionSpace(Record):
    """
    Class representing notion's Space - user workplace.
    """

    _type = "space"
    _table = "space"
    _str_fields = "name", "domain"
    _child_list_key = "pages"

    name = field_map("name")
    domain = field_map("domain")
    icon = field_map("icon")

    @property
    def pages(self) -> list:
        # The page list includes pages the current user
        # might not have permissions on, so it's slow to query.
        # Instead, we just filter for pages with the space as the parent.
        return self._client.search_pages_with_parent(self.id)

    @property
    def users(self) -> list:
        ids = [p["user_id"] for p in self.get("permissions")]
        self._client.refresh_records(notion_user=ids)
        return [self._client.get_user(uid) for uid in ids]

    def add_page(
            self,
            title,
            type: str = "page",
            shared: bool = False) -> Union[PageBlock, CollectionViewPageBlock]:
        """
        Create new page.


        Arguments
        ---------
        title : str
            Title for the newly created page.

        type : str, optional
            Type of the page. Must be one of "page" or "collection_view_page".
            Defaults to "page".

        shared : bool, optional
            Whether or not the page should be shared (public).
            TODO: is it true?
            Defaults to False.
        """
        perms = [{
            "role": "editor",
            "type": "user_permission",
            "user_id": self._client.current_user.id,
        }]

        if shared:
            perms = [{"role": "editor", "type": "space_permission"}]

        page_id = self._client.create_record("block",
                                             self,
                                             type=type,
                                             permissions=perms)
        page = self._client.get_block(page_id)
        page.title = title
        return page
コード例 #11
0
class CodeBlock(BasicBlock):

    _type = "code"

    language = property_map("language")
    wrap = field_map("format.code_wrap")
コード例 #12
0
class CalloutBlock(BasicBlock):

    _type = "callout"

    icon = field_map("format.page_icon")
コード例 #13
0
class Block(Record):
    """
    Base class for every kind of notion block object.

    Most data in Notion is stored as a "block". That includes pages
    and all the individual elements within a page. These blocks have
    different types, and in some cases we create subclasses of this
    class to represent those types.

    Attributes on the `Block` are mapped to useful attributes of the
    server-side data structure, as properties, so you can get and set
    values on the API just by reading/writing attributes on these classes.

    We store a shared local cache on the `NotionClient` object
    of all block data, and reference that as needed from here.
    Data can be refreshed from the server using the `refresh` method.
    """

    _table = "block"
    _type = "block"
    _str_fields = "type"

    # we'll mark it as an alias if we load the Block
    # as a child of a page that is not its parent
    _alias_parent = None

    child_list_key = "content"

    type = field_map("type")
    alive = field_map("alive")

    @property
    def children(self):
        if not self._children:
            children_ids = self.get("content", [])
            self._client.refresh_records(block=children_ids)
            # TODO: can we do something about that without breaking
            #       the current code layout?
            from notion.block.children import Children

            self._children = Children(parent=self)
        return self._children

    @property
    def is_alias(self):
        return self._alias_parent is not None

    @property
    def parent(self):
        parent_id = self._alias_parent
        parent_table = "block"

        if not self.is_alias:
            parent_id = self.get("parent_id")
            parent_table = self.get("parent_table")

        getter = getattr(self._client, f"get_{parent_table}")
        if getter:
            return getter(parent_id)

        return None

    def _convert_diff_to_changelist(self, difference, old_val, new_val):
        # TODO: cached property?
        mappers = {}
        for name in dir(self.__class__):
            field = getattr(self.__class__, name)
            if isinstance(field, Mapper):
                mappers[name] = field

        changed_fields = set()
        changes = []
        remaining = []
        content_changed = False

        for d in deepcopy(difference):
            operation, path, values = d

            # normalize path
            path = path if path else []
            path = path.split(".") if isinstance(path, str) else path
            if operation in ["add", "remove"]:
                path.append(values[0][0])
            while isinstance(path[-1], int):
                path.pop()
            path = ".".join(map(str, path))

            # check whether it was content that changed
            if path == "content":
                content_changed = True
                continue

            # check whether the value changed matches one of our mapped fields/properties
            fields = [(name, field) for name, field in mappers.items()
                      if path.startswith(field.path)]
            if fields:
                changed_fields.add(fields[0])
                continue

            remaining.append(d)

        if content_changed:

            old = deepcopy(old_val.get("content", []))
            new = deepcopy(new_val.get("content", []))

            # track what's been added and removed
            removed = set(old) - set(new)
            added = set(new) - set(old)
            for id in removed:
                changes.append(("content_removed", "content", id))
            for id in added:
                changes.append(("content_added", "content", id))

            # ignore the added/removed items, and see whether order has changed
            for id in removed:
                old.remove(id)
            for id in added:
                new.remove(id)
            if old != new:
                changes.append(("content_reordered", "content", (old, new)))

        for name, field in changed_fields:
            old = field.api_to_python(get_by_path(field.path, old_val))
            new = field.api_to_python(get_by_path(field.path, new_val))
            changes.append(("changed_field", name, (old, new)))

        return changes + super()._convert_diff_to_changelist(
            remaining, old_val, new_val)

    def get_browseable_url(self) -> str:
        """
        Return direct URL to given Block.


        Returns
        -------
        str
            valid URL
        """
        short_id = self.id.replace("-", "")

        if "page" in self._type:
            return BASE_URL + short_id
        else:
            return self.parent.get_browseable_url() + "#" + short_id

    def remove(self, permanently: bool = False):
        """
        Remove the node from its parent, and mark it as inactive.

        This corresponds to what happens in the Notion UI when you
        delete a block. Note that it doesn't *actually* delete it,
        just orphan it, unless `permanently` is set to True,
        in which case we make an extra call to hard-delete.


        Arguments
        ---------
        permanently : bool, optional
            Whether or not to hard-delete the block.
            Defaults to False.
        """
        if self.is_alias:
            # only remove it from the alias parent's content list
            return self._client.submit_transaction(
                build_operation(
                    id=self._alias_parent,
                    path="content",
                    args={"id": self.id},
                    command="listRemove",
                ))

        with self._client.as_atomic_transaction():
            # Mark the block as inactive
            self._client.submit_transaction(
                build_operation(id=self.id,
                                path=[],
                                args={"alive": False},
                                command="update"))

            # Remove the block's ID from a list on its parent, if needed
            if self.parent.child_list_key:
                self._client.submit_transaction(
                    build_operation(
                        id=self.parent.id,
                        path=[self.parent.child_list_key],
                        args={"id": self.id},
                        command="listRemove",
                        table=self.parent._table,
                    ))

        if permanently:
            self._client.post("deleteBlocks", {
                "blockIds": [self.id],
                "permanentlyDelete": True
            })
            del self._client._store._values["block"][self.id]

    def move_to(self, target_block: "Block", position="last-child"):
        assert position in ["first-child", "last-child", "before", "after"]

        if "child" in position:
            new_parent_id = target_block.id
            new_parent_table = "block"
        else:
            new_parent_id = target_block.get("parent_id")
            new_parent_table = target_block.get("parent_table")

        if position in ["first-child", "before"]:
            list_command = "listBefore"
        else:
            list_command = "listAfter"

        list_args = {"id": self.id}
        if position in ["before", "after"]:
            list_args[position] = target_block.id

        with self._client.as_atomic_transaction():

            # First, remove the node, before we re-insert and re-activate it at the target location
            self.remove()

            if not self.is_alias:
                # Set the parent_id of the moving block to the new parent, and mark it as active again
                self._client.submit_transaction(
                    build_operation(
                        id=self.id,
                        path=[],
                        args={
                            "alive": True,
                            "parent_id": new_parent_id,
                            "parent_table": new_parent_table,
                        },
                        command="update",
                    ))
            else:
                self._alias_parent = new_parent_id

            # Add the moving block's ID to the "content" list of the new parent
            self._client.submit_transaction(
                build_operation(
                    id=new_parent_id,
                    path=["content"],
                    args=list_args,
                    command=list_command,
                ))

        # update the local block cache to reflect the updates
        self._client.refresh_records(block=[
            self.id,
            self.get("parent_id"),
            target_block.id,
            target_block.get("parent_id"),
        ])

    def change_lock(self, locked: bool):
        """
        Set or free the lock according to the value passed in `locked`.


        Arguments
        ---------
        locked : bool
            Whether or not to lock the block.
        """
        args = dict(block_locked=locked,
                    block_locked_by=self._client.current_user.id)

        with self._client.as_atomic_transaction():
            self._client.submit_transaction(
                build_operation(
                    id=self.id,
                    path=["format"],
                    args=args,
                    command="update",
                ))

        # update the local block cache to reflect the updates
        self._client.refresh_records(block=[self.id])
コード例 #14
0
ファイル: basic.py プロジェクト: Joss/notion-py
class CollectionBlock(Block):
    """
    Collection Block.
    """

    _type = "collection"
    _table = "collection"
    _str_fields = "name"

    cover = field_map("cover")
    name = markdown_field_map("name")
    description = markdown_field_map("description")

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._templates = None

    @property
    def templates(self) -> Templates:
        if not self._templates:
            template_ids = self.get("template_pages", [])
            self._client.refresh_records(block=template_ids)
            self._templates = Templates(parent=self)
        return self._templates

    def get_schema_properties(self) -> list:
        """
        Fetch a flattened list of all properties in the collection's schema.


        Returns
        -------
        list
            All properties.
        """
        properties = []
        for block_id, item in self.get("schema").items():
            slug = slugify(item["name"])
            prop = {"id": block_id, "slug": slug, **item}
            properties.append(prop)

        return properties

    def get_schema_property(self, identifier: str) -> Optional[dict]:
        """
        Look up a property in the collection's schema
        by "property id" (generally a 4-char string),
        or name (human-readable -- there may be duplicates
        so we pick the first match we find).


        Attributes
        ----------
        identifier : str
            Value used for searching the prop.
            Can be set to ID, slug or title (if property type is also title).


        Returns
        -------
        dict, optional
            Schema of the property if found, or None.
        """
        for prop in self.get_schema_properties():
            if identifier == prop["id"] or slugify(identifier) == prop["slug"]:
                return prop
            if identifier == "title" and prop["type"] == "title":
                return prop
        return None

    def add_row(self, update_views=True, **kwargs) -> "CollectionRowBlock":
        """
        Create a new empty CollectionRowBlock
        under this collection, and return the instance.


        Arguments
        ---------
        update_views : bool, optional
            Whether or not to update the views after
            adding the row to Collection.
            Defaults to True.

        kwargs : dict, optional
            Additional pairs of keys and values set in
            newly created CollectionRowBlock.
            Defaults to empty dict()


        Returns
        -------
        CollectionRowBlock
            Added row.
        """

        row_id = self._client.create_record("block", self, type="page")
        row = CollectionRowBlock(self._client, row_id)

        with self._client.as_atomic_transaction():
            for key, val in kwargs.items():
                setattr(row, key, val)
            if update_views:
                # make sure the new record is inserted at the end of each view
                for view in self.parent.views:
                    if isinstance(view, CalendarView):
                        continue
                    view.set("page_sort", view.get("page_sort", []) + [row_id])

        return row

    @property
    def parent(self):
        """
        Get parent block.


        Returns
        -------
        Block
            Parent block.
        """
        assert self.get("parent_table") == "block"
        return self._client.get_block(self.get("parent_id"))

    def _get_a_collection_view(self):
        """
        Get an arbitrary collection view for this collection, to allow querying.
        """
        parent = self.parent
        assert isinstance(parent, CollectionViewBlock)
        assert len(parent.views) > 0
        return parent.views[0]

    def query(self, **kwargs):
        """
        Run a query inline and return the results.


        Returns
        -------
        CollectionQueryResult
            Result of passed query.
        """
        return CollectionQuery(self, self._get_a_collection_view(),
                               **kwargs).execute()

    def get_rows(self, **kwargs):
        return self.query(**kwargs)

    def _convert_diff_to_changelist(self, difference, old_val, new_val):
        changes = []
        remaining = []

        for operation, path, values in difference:
            if path == "rows":
                changes.append((operation, path, values))
            else:
                remaining.append((operation, path, values))

        return changes + super()._convert_diff_to_changelist(
            remaining, old_val, new_val)
コード例 #15
0
class UploadBlock(EmbedBlock):
    """
    Upload Block.
    """

    file_id = field_map("file_ids.0")

    def upload_file(self, path: str):
        """
        Upload a file and embed it in Notion.


        Arguments
        ---------
        path : str
            Valid path to a file.


        Raises
        ------
        HTTPError
            On API error.
        """

        content_type = guess_type(path)[0] or "text/plain"
        file_name = os.path.split(path)[-1]

        data = {
            "bucket": "secure",
            "name": file_name,
            "contentType": content_type
        }
        resp = self._client.post("getUploadFileUrl", data)
        resp.raise_for_status()
        resp_data = resp.json()
        url = resp_data["url"]
        signed_url = resp_data["signedPutUrl"]

        with open(path, mode="rb") as f:
            headers = {"Content-Type": content_type}
            resp = self._client.put(signed_url, data=f, headers=headers)
            resp.raise_for_status()

        query = urlencode({
            "cache": "v2",
            "name": file_name,
            "id": self._id,
            "table": self._table,
            "userId": self._client.current_user.id,
        })
        query_url = f"{url}?{query}"

        self.source = query_url
        self.display_source = query_url
        self.file_id = urlparse(url).path.split("/")[2]

    def download_file(self, path: str):
        """
        Download a file.


        Arguments
        ---------
        path : str
            Path for saving file.


        Raises
        ------
        HTTPError
            On API error.
        """

        record_data = self._get_record_data()
        source = record_data["properties"]["source"]
        s3_url = from_list(source)
        file_name = s3_url.split("/")[-1]

        params = {
            "cache": "v2",
            "name": file_name,
            "id": self._id,
            "table": self._table,
            "userId": self._client.current_user.id,
            "download": True,
        }

        url = SIGNED_URL_PREFIX + quote(s3_url, safe="")
        resp = self._client.session.get(url, params=params, stream=True)
        resp.raise_for_status()

        with open(path, "wb") as f:
            for chunk in resp.iter_content(chunk_size=4096):
                f.write(chunk)