From 35002613a9e58846c1125acf25d1330d3d8d11c2 Mon Sep 17 00:00:00 2001 From: Benature Date: Sun, 16 Mar 2025 13:40:53 +0800 Subject: [PATCH 01/12] feat: create_highlight --- omnivoreql/omnivoreql.py | 24 +++++++++++++++++++++- omnivoreql/queries/CreateHighlight.graphql | 24 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/omnivoreql/omnivoreql.py b/omnivoreql/omnivoreql.py index b7368e3..48c99b8 100644 --- a/omnivoreql/omnivoreql.py +++ b/omnivoreql/omnivoreql.py @@ -1,6 +1,7 @@ import uuid +import shortuuid import os -from typing import List, Optional +from typing import List, Optional, Literal from gql.transport.requests import RequestsHTTPTransport from gql import gql, Client from dataclasses import asdict @@ -279,3 +280,24 @@ def set_page_labels_by_ids(self, page_id: str, label_ids: List[str]) -> dict: } }, ) + def create_highlight(self, article_id: str, annotation: str, + highlight_type: Literal["HIGHLIGHT", "NOTE"]): + """ + Create a new highlight. + + :param article_id: The ID of the article to create the highlight for. + :param annotation: The annotation of the highlight. + :param highlight_type: The type of the highlight. + """ + return self.client.execute( + self._get_query("CreateHighlight"), + variable_values={ + "input": { + "annotation": annotation, + "articleId": article_id, + "id": str(uuid.uuid4()), + "shortId": str(shortuuid.ShortUUID().random(length=8)), + "type": highlight_type + } + }, + ) diff --git a/omnivoreql/queries/CreateHighlight.graphql b/omnivoreql/queries/CreateHighlight.graphql index e9717ac..f023a21 100644 --- a/omnivoreql/queries/CreateHighlight.graphql +++ b/omnivoreql/queries/CreateHighlight.graphql @@ -11,3 +11,27 @@ mutation CreateHighlight($input: CreateHighlightInput!) { } } } + +fragment HighlightFields on Highlight { + id + type + shortId + quote + prefix + suffix + patch + color + annotation + createdByMe + createdAt + updatedAt + sharedAt + highlightPositionPercent + highlightPositionAnchorIndex + labels { + id + name + color + createdAt + } +} \ No newline at end of file From c985f9c8a4a3fe541f0ae75a44b73d547450723c Mon Sep 17 00:00:00 2001 From: Benature Date: Sat, 22 Mar 2025 22:33:00 +0800 Subject: [PATCH 02/12] rename package --- omnivore_api/__init__.py | 4 + {omnivoreql => omnivore_api}/models.py | 0 {omnivoreql => omnivore_api}/omnivoreql.py | 81 ++++++++++++------- .../queries/ApplyLabels.graphql | 0 .../queries/ArchiveSavedItem.graphql | 0 .../queries/ArticleContent.graphql | 0 .../queries/CreateHighlight.graphql | 0 .../queries/CreateLabel.graphql | 0 .../queries/DeleteHighlight.graphql | 0 .../queries/DeleteLabel.graphql | 0 .../queries/DeleteSavedItem.graphql | 0 .../queries/GetSubscriptions.graphql | 0 .../queries/Labels.graphql | 0 .../queries/MergeHighlight.graphql | 0 .../queries/ReadingProgressMutation.graphql | 0 .../queries/SavePage.graphql | 0 .../queries/SaveUrl.graphql | 0 .../queries/Search.graphql | 0 .../queries/TypeAheadSearch.graphql | 0 .../queries/UpdateHighlight.graphql | 0 .../queries/UpdateLabel.graphql | 0 .../queries/UpdatePage.graphql | 0 .../queries/UpdatesSince.graphql | 0 .../queries/ValidateUsername.graphql | 0 .../queries/Viewer.graphql | 0 .../queries/schema.graphqls | 1 + omnivoreql/__init__.py | 2 - 27 files changed, 58 insertions(+), 30 deletions(-) create mode 100644 omnivore_api/__init__.py rename {omnivoreql => omnivore_api}/models.py (100%) rename {omnivoreql => omnivore_api}/omnivoreql.py (82%) rename {omnivoreql => omnivore_api}/queries/ApplyLabels.graphql (100%) rename {omnivoreql => omnivore_api}/queries/ArchiveSavedItem.graphql (100%) rename {omnivoreql => omnivore_api}/queries/ArticleContent.graphql (100%) rename {omnivoreql => omnivore_api}/queries/CreateHighlight.graphql (100%) rename {omnivoreql => omnivore_api}/queries/CreateLabel.graphql (100%) rename {omnivoreql => omnivore_api}/queries/DeleteHighlight.graphql (100%) rename {omnivoreql => omnivore_api}/queries/DeleteLabel.graphql (100%) rename {omnivoreql => omnivore_api}/queries/DeleteSavedItem.graphql (100%) rename {omnivoreql => omnivore_api}/queries/GetSubscriptions.graphql (100%) rename {omnivoreql => omnivore_api}/queries/Labels.graphql (100%) rename {omnivoreql => omnivore_api}/queries/MergeHighlight.graphql (100%) rename {omnivoreql => omnivore_api}/queries/ReadingProgressMutation.graphql (100%) rename {omnivoreql => omnivore_api}/queries/SavePage.graphql (100%) rename {omnivoreql => omnivore_api}/queries/SaveUrl.graphql (100%) rename {omnivoreql => omnivore_api}/queries/Search.graphql (100%) rename {omnivoreql => omnivore_api}/queries/TypeAheadSearch.graphql (100%) rename {omnivoreql => omnivore_api}/queries/UpdateHighlight.graphql (100%) rename {omnivoreql => omnivore_api}/queries/UpdateLabel.graphql (100%) rename {omnivoreql => omnivore_api}/queries/UpdatePage.graphql (100%) rename {omnivoreql => omnivore_api}/queries/UpdatesSince.graphql (100%) rename {omnivoreql => omnivore_api}/queries/ValidateUsername.graphql (100%) rename {omnivoreql => omnivore_api}/queries/Viewer.graphql (100%) rename {omnivoreql => omnivore_api}/queries/schema.graphqls (99%) delete mode 100644 omnivoreql/__init__.py diff --git a/omnivore_api/__init__.py b/omnivore_api/__init__.py new file mode 100644 index 0000000..f746c70 --- /dev/null +++ b/omnivore_api/__init__.py @@ -0,0 +1,4 @@ +from .omnivoreql import OmnivoreQL +from .models import CreateLabelInput + +__version__ = "0.4.0" diff --git a/omnivoreql/models.py b/omnivore_api/models.py similarity index 100% rename from omnivoreql/models.py rename to omnivore_api/models.py diff --git a/omnivoreql/omnivoreql.py b/omnivore_api/omnivoreql.py similarity index 82% rename from omnivoreql/omnivoreql.py rename to omnivore_api/omnivoreql.py index 48c99b8..75016ec 100644 --- a/omnivoreql/omnivoreql.py +++ b/omnivore_api/omnivoreql.py @@ -9,10 +9,11 @@ class OmnivoreQL: + def __init__( self, api_token: str, - graphql_endpoint_url: str = "https://api-prod.omnivore.app/api/graphql", + graphql_endpoint_url: str, ) -> None: """ Initialize a new instance of the GraphQL client. @@ -22,25 +23,30 @@ def __init__( """ transport = RequestsHTTPTransport( url=graphql_endpoint_url, - headers={"content-type": "application/json", "authorization": api_token}, + headers={ + "content-type": "application/json", + "authorization": api_token + }, use_json=True, ) - self.client = Client(transport=transport, fetch_schema_from_transport=False) + self.client = Client(transport=transport, + fetch_schema_from_transport=False) self.queries = {} def _get_query(self, query_name: str) -> str: if query_name not in self.queries: current_dir = os.path.dirname(os.path.abspath(__file__)) - query_file_path = os.path.join(current_dir, f"queries/{query_name}.graphql") + query_file_path = os.path.join(current_dir, + f"queries/{query_name}.graphql") with open(query_file_path, "r") as file: self.queries[query_name] = gql(file.read()) return self.queries[query_name] def save_url( - self, - url: str, - labels: Optional[List[str]] = None, - client_request_id: str = str(uuid.uuid4()), + self, + url: str, + labels: Optional[List[str]] = None, + client_request_id: str = str(uuid.uuid4()), ): """ Save a URL to Omnivore. @@ -62,7 +68,10 @@ def save_url( }, ) - def save_page(self, url: str, original_content: str, labels: List[str] = None): + def save_page(self, + url: str, + original_content: str, + labels: List[str] = None): """ Save a page with html content to Omnivore. @@ -130,7 +139,11 @@ def get_articles( }, ) - def get_article(self, username: str, slug: str, format: str = None, include_content: bool = False): + def get_article(self, + username: str, + slug: str, + format: str = None, + include_content: bool = False): """ Get an article by username and slug. @@ -157,7 +170,12 @@ def archive_article(self, article_id: str, to_archive: bool = True): """ return self.client.execute( self._get_query("ArchiveSavedItem"), - variable_values={"input": {"linkId": article_id, "archived": to_archive}}, + variable_values={ + "input": { + "linkId": article_id, + "archived": to_archive + } + }, ) def unarchive_article(self, article_id: str): @@ -176,7 +194,12 @@ def delete_article(self, article_id: str): """ return self.client.execute( self._get_query("DeleteSavedItem"), - variable_values={"input": {"articleID": article_id, "bookmark": False}}, + variable_values={ + "input": { + "articleID": article_id, + "bookmark": False + } + }, ) def create_label(self, label: CreateLabelInput): @@ -190,9 +213,11 @@ def create_label(self, label: CreateLabelInput): variable_values={"input": asdict(label)}, ) - def update_label( - self, label_id: str, name: str, color: str, description: str = None - ): + def update_label(self, + label_id: str, + name: str, + color: str, + description: str = None): """ Update a label. @@ -224,9 +249,8 @@ def delete_label(self, label_id: str): variable_values={"id": label_id}, ) - def set_page_labels( - self, page_id: str, labels: List[CreateLabelInput] - ) -> dict: + def set_page_labels(self, page_id: str, + labels: List[CreateLabelInput]) -> dict: """ Set labels for a page. @@ -235,7 +259,8 @@ def set_page_labels( """ return self.set_page_labels_by_fields(page_id, labels) - def set_page_labels_by_fields(self, page_id: str, labels: List[dict]) -> dict: + def set_page_labels_by_fields(self, page_id: str, + labels: List[dict]) -> dict: """ Set labels for a page. @@ -246,13 +271,11 @@ def set_page_labels_by_fields(self, page_id: str, labels: List[dict]) -> dict: for label in labels: if isinstance(label, CreateLabelInput): label = asdict(label) - parsed_labels.append( - { - "name": label["name"], - "color": label["color"], - "description": label["description"], - } - ) + parsed_labels.append({ + "name": label["name"], + "color": label["color"], + "description": label["description"], + }) return self.client.execute( self._get_query("ApplyLabels"), @@ -264,7 +287,8 @@ def set_page_labels_by_fields(self, page_id: str, labels: List[dict]) -> dict: }, ) - def set_page_labels_by_ids(self, page_id: str, label_ids: List[str]) -> dict: + def set_page_labels_by_ids(self, page_id: str, + label_ids: List[str]) -> dict: """ Set labels for a page. @@ -280,7 +304,8 @@ def set_page_labels_by_ids(self, page_id: str, label_ids: List[str]) -> dict: } }, ) - def create_highlight(self, article_id: str, annotation: str, + + def create_highlight(self, article_id: str, annotation: str, highlight_type: Literal["HIGHLIGHT", "NOTE"]): """ Create a new highlight. diff --git a/omnivoreql/queries/ApplyLabels.graphql b/omnivore_api/queries/ApplyLabels.graphql similarity index 100% rename from omnivoreql/queries/ApplyLabels.graphql rename to omnivore_api/queries/ApplyLabels.graphql diff --git a/omnivoreql/queries/ArchiveSavedItem.graphql b/omnivore_api/queries/ArchiveSavedItem.graphql similarity index 100% rename from omnivoreql/queries/ArchiveSavedItem.graphql rename to omnivore_api/queries/ArchiveSavedItem.graphql diff --git a/omnivoreql/queries/ArticleContent.graphql b/omnivore_api/queries/ArticleContent.graphql similarity index 100% rename from omnivoreql/queries/ArticleContent.graphql rename to omnivore_api/queries/ArticleContent.graphql diff --git a/omnivoreql/queries/CreateHighlight.graphql b/omnivore_api/queries/CreateHighlight.graphql similarity index 100% rename from omnivoreql/queries/CreateHighlight.graphql rename to omnivore_api/queries/CreateHighlight.graphql diff --git a/omnivoreql/queries/CreateLabel.graphql b/omnivore_api/queries/CreateLabel.graphql similarity index 100% rename from omnivoreql/queries/CreateLabel.graphql rename to omnivore_api/queries/CreateLabel.graphql diff --git a/omnivoreql/queries/DeleteHighlight.graphql b/omnivore_api/queries/DeleteHighlight.graphql similarity index 100% rename from omnivoreql/queries/DeleteHighlight.graphql rename to omnivore_api/queries/DeleteHighlight.graphql diff --git a/omnivoreql/queries/DeleteLabel.graphql b/omnivore_api/queries/DeleteLabel.graphql similarity index 100% rename from omnivoreql/queries/DeleteLabel.graphql rename to omnivore_api/queries/DeleteLabel.graphql diff --git a/omnivoreql/queries/DeleteSavedItem.graphql b/omnivore_api/queries/DeleteSavedItem.graphql similarity index 100% rename from omnivoreql/queries/DeleteSavedItem.graphql rename to omnivore_api/queries/DeleteSavedItem.graphql diff --git a/omnivoreql/queries/GetSubscriptions.graphql b/omnivore_api/queries/GetSubscriptions.graphql similarity index 100% rename from omnivoreql/queries/GetSubscriptions.graphql rename to omnivore_api/queries/GetSubscriptions.graphql diff --git a/omnivoreql/queries/Labels.graphql b/omnivore_api/queries/Labels.graphql similarity index 100% rename from omnivoreql/queries/Labels.graphql rename to omnivore_api/queries/Labels.graphql diff --git a/omnivoreql/queries/MergeHighlight.graphql b/omnivore_api/queries/MergeHighlight.graphql similarity index 100% rename from omnivoreql/queries/MergeHighlight.graphql rename to omnivore_api/queries/MergeHighlight.graphql diff --git a/omnivoreql/queries/ReadingProgressMutation.graphql b/omnivore_api/queries/ReadingProgressMutation.graphql similarity index 100% rename from omnivoreql/queries/ReadingProgressMutation.graphql rename to omnivore_api/queries/ReadingProgressMutation.graphql diff --git a/omnivoreql/queries/SavePage.graphql b/omnivore_api/queries/SavePage.graphql similarity index 100% rename from omnivoreql/queries/SavePage.graphql rename to omnivore_api/queries/SavePage.graphql diff --git a/omnivoreql/queries/SaveUrl.graphql b/omnivore_api/queries/SaveUrl.graphql similarity index 100% rename from omnivoreql/queries/SaveUrl.graphql rename to omnivore_api/queries/SaveUrl.graphql diff --git a/omnivoreql/queries/Search.graphql b/omnivore_api/queries/Search.graphql similarity index 100% rename from omnivoreql/queries/Search.graphql rename to omnivore_api/queries/Search.graphql diff --git a/omnivoreql/queries/TypeAheadSearch.graphql b/omnivore_api/queries/TypeAheadSearch.graphql similarity index 100% rename from omnivoreql/queries/TypeAheadSearch.graphql rename to omnivore_api/queries/TypeAheadSearch.graphql diff --git a/omnivoreql/queries/UpdateHighlight.graphql b/omnivore_api/queries/UpdateHighlight.graphql similarity index 100% rename from omnivoreql/queries/UpdateHighlight.graphql rename to omnivore_api/queries/UpdateHighlight.graphql diff --git a/omnivoreql/queries/UpdateLabel.graphql b/omnivore_api/queries/UpdateLabel.graphql similarity index 100% rename from omnivoreql/queries/UpdateLabel.graphql rename to omnivore_api/queries/UpdateLabel.graphql diff --git a/omnivoreql/queries/UpdatePage.graphql b/omnivore_api/queries/UpdatePage.graphql similarity index 100% rename from omnivoreql/queries/UpdatePage.graphql rename to omnivore_api/queries/UpdatePage.graphql diff --git a/omnivoreql/queries/UpdatesSince.graphql b/omnivore_api/queries/UpdatesSince.graphql similarity index 100% rename from omnivoreql/queries/UpdatesSince.graphql rename to omnivore_api/queries/UpdatesSince.graphql diff --git a/omnivoreql/queries/ValidateUsername.graphql b/omnivore_api/queries/ValidateUsername.graphql similarity index 100% rename from omnivoreql/queries/ValidateUsername.graphql rename to omnivore_api/queries/ValidateUsername.graphql diff --git a/omnivoreql/queries/Viewer.graphql b/omnivore_api/queries/Viewer.graphql similarity index 100% rename from omnivoreql/queries/Viewer.graphql rename to omnivore_api/queries/Viewer.graphql diff --git a/omnivoreql/queries/schema.graphqls b/omnivore_api/queries/schema.graphqls similarity index 99% rename from omnivoreql/queries/schema.graphqls rename to omnivore_api/queries/schema.graphqls index 9bf3f87..69f1b39 100644 --- a/omnivoreql/queries/schema.graphqls +++ b/omnivore_api/queries/schema.graphqls @@ -105,6 +105,7 @@ type Article { uploadFileId: ID url: String! wordsCount: Int + aiSummary: String } type ArticleEdge { diff --git a/omnivoreql/__init__.py b/omnivoreql/__init__.py deleted file mode 100644 index fc59754..0000000 --- a/omnivoreql/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .omnivoreql import OmnivoreQL -from .models import CreateLabelInput \ No newline at end of file From e0065ae4b715f91a71b03138b796f41691200e68 Mon Sep 17 00:00:00 2001 From: Benature Date: Sat, 22 Mar 2025 22:35:08 +0800 Subject: [PATCH 03/12] rename as OmnivoreAPI --- README.md | 4 +++- omnivore_api/__init__.py | 2 +- omnivore_api/{omnivoreql.py => api.py} | 2 +- setup.py | 20 ++++++-------------- 4 files changed, 11 insertions(+), 17 deletions(-) rename omnivore_api/{omnivoreql.py => api.py} (99%) diff --git a/README.md b/README.md index d208f8f..25670f4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# OmnivoreQL: Omnivore API client for Python +# OmnivoreAPI: Omnivore API client for Python + +**Forked from OmnivoreQL** ![OmnivoreQL Icon](https://github.com/yazdipour/OmnivoreQL/assets/8194807/d51d462d-4f5a-4031-980e-1faa5ca3f6e0) diff --git a/omnivore_api/__init__.py b/omnivore_api/__init__.py index f746c70..5355421 100644 --- a/omnivore_api/__init__.py +++ b/omnivore_api/__init__.py @@ -1,4 +1,4 @@ -from .omnivoreql import OmnivoreQL +from .api import OmnivoreAPI from .models import CreateLabelInput __version__ = "0.4.0" diff --git a/omnivore_api/omnivoreql.py b/omnivore_api/api.py similarity index 99% rename from omnivore_api/omnivoreql.py rename to omnivore_api/api.py index 75016ec..b396b19 100644 --- a/omnivore_api/omnivoreql.py +++ b/omnivore_api/api.py @@ -8,7 +8,7 @@ from .models import CreateLabelInput -class OmnivoreQL: +class OmnivoreAPI: def __init__( self, diff --git a/setup.py b/setup.py index 51aa978..bf2f822 100644 --- a/setup.py +++ b/setup.py @@ -4,16 +4,8 @@ def get_latest_git_tag(): try: - version = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"]) - version = version.strip().decode( - "utf-8" - ) # Remove trailing newline and decode bytes to string - - # Remove the 'v' from the tag - if version.startswith("v"): - version = version[1:] - - return version + import omnivore_api + return omnivore_api.__version__ except Exception as e: print(f"An exception occurred while getting the latest git tag: {e}") return None @@ -32,18 +24,18 @@ def read_requirements(): } setup( - name="omnivoreql", + name="omnivore_api", version=VERSION, description="Omnivore API Client for Python", - author="Shahriar Yazdipour", - author_email="git@yazdipour.com", + author="Benature", + author_email="", packages=find_packages(), long_description=open("README.md").read(), long_description_content_type="text/markdown", license="MIT", keywords="omnivore api readlater graphql gql client", platforms="any", - url="https://github.com/yazdipour/OmnivoreQL", + url="https://github.com/Benature/OmnivoreAPI", project_urls=PROJECT_URLS, include_package_data=True, python_requires=">=3", From 4a32343d88391b35477c17d36f1397963c04da48 Mon Sep 17 00:00:00 2001 From: Benature Date: Sat, 22 Mar 2025 22:39:44 +0800 Subject: [PATCH 04/12] github workflow --- .github/workflows/pypi.yml | 95 +++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 30 ----------- omnivore_api/__init__.py | 2 +- 3 files changed, 96 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/pypi.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml new file mode 100644 index 0000000..149f194 --- /dev/null +++ b/.github/workflows/pypi.yml @@ -0,0 +1,95 @@ +name: Publish Python 🐍 distribution 📦 to PyPI +# https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ + +on: push + +jobs: + build: + name: Build distribution 📦 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install pypa/build + run: >- + python3 -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: python3 -m build + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish-to-pypi: + name: Publish to PyPI + if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes + needs: + - build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/omnivore_api # Replace with your PyPI project name + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + github-release: + name: >- + Sign the Python 🐍 distribution 📦 with Sigstore + and upload them to GitHub Release + needs: + - publish-to-pypi + runs-on: ubuntu-latest + + permissions: + contents: write # IMPORTANT: mandatory for making GitHub Releases + id-token: write # IMPORTANT: mandatory for sigstore + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Sign the dists with Sigstore + uses: sigstore/gh-action-sigstore-python@v3.0.0 + with: + inputs: >- + ./dist/*.tar.gz + ./dist/*.whl + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + gh release create + "$GITHUB_REF_NAME" + --repo "$GITHUB_REPOSITORY" + --notes "" + - name: Upload artifact signatures to GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + # Upload to GitHub Release using the `gh` CLI. + # `dist/` contains the built packages, and the + # sigstore-produced signatures and certificates. + run: >- + gh release upload + "$GITHUB_REF_NAME" dist/** + --repo "$GITHUB_REPOSITORY" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index c1c346b..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: 🚀 Releasing new version to PyPI - -on: - push: - tags: - - '*' - -permissions: - contents: read - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build setuptools wheel - - name: Build package - run: python setup.py sdist bdist_wheel - - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/omnivore_api/__init__.py b/omnivore_api/__init__.py index 5355421..0a8d87a 100644 --- a/omnivore_api/__init__.py +++ b/omnivore_api/__init__.py @@ -1,4 +1,4 @@ from .api import OmnivoreAPI from .models import CreateLabelInput -__version__ = "0.4.0" +__version__ = "0.4.0-alpha" From 731e03d158bcbb3bc8e74d750d31b2c48f9f39dc Mon Sep 17 00:00:00 2001 From: Benature Date: Sun, 23 Mar 2025 00:51:09 +0800 Subject: [PATCH 05/12] rename --- .devcontainer/devcontainer.json | 2 +- .github/FUNDING.yml | 4 -- MANIFEST.in | 2 +- README.md | 44 +++++++++---------- setup.py | 4 +- tests/test_omnivoreql.py | 77 +++++++++++++++++---------------- 6 files changed, 65 insertions(+), 68 deletions(-) delete mode 100644 .github/FUNDING.yml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3de3568..3242129 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/python { - "name": "omnivoreql", + "name": "omnivore_api", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile "image": "mcr.microsoft.com/devcontainers/python:1-3.12", // Features to add to the dev container. More info: https://containers.dev/features. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index adfaa91..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,4 +0,0 @@ -# These are supported funding model platforms - -github: [yazdipour] -custom: ['https://paypal.me/zhahriar'] diff --git a/MANIFEST.in b/MANIFEST.in index e5c6ec2..7abb4e4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,7 +2,7 @@ include MANIFEST.in include LICENSE include README.md include requirements.txt -recursive-include omnivoreql * +recursive-include omnivore_api * # exclude from sdist exclude CONTRIBUTING.md \ No newline at end of file diff --git a/README.md b/README.md index 25670f4..95bddeb 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,52 @@ # OmnivoreAPI: Omnivore API client for Python -**Forked from OmnivoreQL** +**Forked from OmnivoreAPI** -![OmnivoreQL Icon](https://github.com/yazdipour/OmnivoreQL/assets/8194807/d51d462d-4f5a-4031-980e-1faa5ca3f6e0) +![OmnivoreAPI Icon](https://github.com/Benature/OmnivoreAPI/assets/8194807/d51d462d-4f5a-4031-980e-1faa5ca3f6e0) This is a Python client for the [Omnivore API](https://omnivore.app). -[![Tests](https://github.com/yazdipour/OmnivoreQL/actions/workflows/test.yml/badge.svg)](https://github.com/yazdipour/OmnivoreQL/actions/workflows/test.yml) -[![PyPI version](https://badge.fury.io/py/omnivoreql.svg)](https://pypi.org/project/omnivoreql/) +[![Tests](https://github.com/Benature/OmnivoreAPI/actions/workflows/test.yml/badge.svg)](https://github.com/Benature/OmnivoreAPI/actions/workflows/test.yml) +[![PyPI version](https://badge.fury.io/py/omnivore_api.svg)](https://pypi.org/project/omnivore_api/) ## How to use -To use omnivoreql in your Python project, you can follow these steps: +To use omnivore_api in your Python project, you can follow these steps: -Install the omnivoreql package using pip: +Install the omnivore_api package using pip: ```bash -pip install omnivoreql +pip install omnivore_api ``` Import the package into your project and Create a new instance of the client: ```python -from omnivoreql import OmnivoreQL +from omnivore_api import OmnivoreAPI -omnivoreql_client = OmnivoreQL("your_api_token_here") +omnivore = OmnivoreAPI("your_api_token_here", "your_api_url_here") ``` -Use the methods of the OmnivoreQL class to interact with the Omnivore API. +Use the methods of the OmnivoreAPI class to interact with the Omnivore API. ```python -profile = omnivoreql_client.get_profile() +profile = omnivore.get_profile() -saved_page = omnivoreql_client.save_url("https://www.google.com") -saved_page_with_label = omnivoreql_client.save_url("https://www.google.com", ["label1", "label2"]) +saved_page = omnivore.save_url("https://www.google.com") +saved_page_with_label = omnivore.save_url("https://www.google.com", ["label1", "label2"]) -articles = omnivoreql_client.get_articles() +articles = omnivore.get_articles() username = profile['me']['profile']['username'] slug = articles['search']['edges'][0]['node']['slug'] -articles = omnivoreql_client.get_article(username, slug) +articles = omnivore.get_article(username, slug) -subscriptions = omnivoreql_client.get_subscriptions() +subscriptions = omnivore.get_subscriptions() -labels = omnivoreql_client.get_labels() -from omnivoreql import CreateLabelInput -omnivoreql_client.create_label(CreateLabelInput("label1", "#00ff00", "This is label description")) +labels = omnivore.get_labels() +from omnivore_api import CreateLabelInput +omnivore.create_label(CreateLabelInput("label1", "#00ff00", "This is label description")) ``` ## Documentation @@ -59,8 +59,8 @@ omnivoreql_client.create_label(CreateLabelInput("label1", "#00ff00", "This is la If you find this project useful, you can support it by becoming a sponsor. Your contribution will help maintain the project and keep it up to date. -[![GitHub stars](https://img.shields.io/github/stars/yazdipour/omnivoreql.svg?style=social&label=Star)](https://github.com/yazdipour/omnivoreql/stargazers) -[![Github Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/yazdipour) +[![GitHub stars](https://img.shields.io/github/stars/Benature/omnivore_api.svg?style=social&label=Star)](https://github.com/Benature/omnivore_api/stargazers) +[![Github Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/Benature) ## License @@ -70,4 +70,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file Check out our growth in the community: -[![Star History Chart](https://api.star-history.com/svg?repos=yazdipour/OmnivoreQL&type=Date)](https://star-history.com/#yazdipour/OmnivoreQL&Date) +[![Star History Chart](https://api.star-history.com/svg?repos=Benature/OmnivoreAPI&type=Date)](https://star-history.com/#Benature/OmnivoreAPI&Date) diff --git a/setup.py b/setup.py index bf2f822..4e0411a 100644 --- a/setup.py +++ b/setup.py @@ -19,8 +19,8 @@ def read_requirements(): VERSION = get_latest_git_tag() or "0.0.1" # Fallback version PROJECT_URLS = { - "Bug Tracker": "https://github.com/yazdipour/OmnivoreQL/issues", - "Source Code": "https://github.com/yazdipour/OmnivoreQL", + "Bug Tracker": "https://github.com/Benature/OmnivoreAPI/issues", + "Source Code": "https://github.com/Benature/OmnivoreAPI", } setup( diff --git a/tests/test_omnivoreql.py b/tests/test_omnivoreql.py index bb06914..ccbe21f 100644 --- a/tests/test_omnivoreql.py +++ b/tests/test_omnivoreql.py @@ -2,24 +2,23 @@ import unittest import sys from dotenv import load_dotenv -from omnivoreql.omnivoreql import OmnivoreQL -from omnivoreql.models import CreateLabelInput - +from omnivore_api.api import OmnivoreAPI +from omnivore_api.models import CreateLabelInput """ -Unit tests for the OmnivoreQL client. +Unit tests for the OmnivoreAPI client. To run the tests, execute the following command: python -m unittest discover -s tests """ -class TestOmnivoreQL(unittest.TestCase): +class TestOmnivoreAPI(unittest.TestCase): @classmethod def setUpClass(cls): """ - Set up class method for unit tests of OmnivoreQL. + Set up class method for unit tests of OmnivoreAPI. - This method initializes the OmnivoreQL client with an API token. + This method initializes the OmnivoreAPI client with an API token. The 'OMNIVORE_API_TOKEN' must be specified either in a '.env' file located in the same directory as this script or passed directly when executing the test script. Example command to run tests with an environment variable: @@ -28,12 +27,12 @@ def setUpClass(cls): Raises: ValueError: If the 'OMNIVORE_API_TOKEN' is not set. """ - print("\nStarting OmnivoreQL tests...\n") + print("\nStarting OmnivoreAPI tests...\n") api_token = cls.getEnvVariable("OMNIVORE_API_TOKEN") if api_token is None: raise ValueError("OMNIVORE_API_TOKEN is not set") print(f"OMNIVORE_API_TOKEN: {api_token[:4]}") - cls.client = OmnivoreQL(api_token) + cls.client = OmnivoreAPI(api_token) cls.sample_label = None # clean_up_created_labels from previous tests try: @@ -47,8 +46,8 @@ def setUpClass(cls): print(f"Error cleaning up labels: {e}") if not cls.sample_label: cls.sample_label = cls.client.create_label( - CreateLabelInput(str(hash("test_update_label")), "#FF0000") - )["createLabel"]["label"] + CreateLabelInput(str(hash("test_update_label")), + "#FF0000"))["createLabel"]["label"] @staticmethod def getEnvVariable(variable_name): @@ -67,7 +66,8 @@ def test_get_profile(self): def test_save_url(self): # When - result = self.client.save_url("https://github.com/yazdipour/OmnivoreQL", ["testLabel"]) + result = self.client.save_url( + "https://github.com/yazdipour/OmnivoreAPI", ["testLabel"]) # Then self.assertIsNotNone(result) self.assertNotIn("errorCodes", result["saveUrl"]) @@ -75,7 +75,8 @@ def test_save_url(self): def test_save_page(self): # When - result = self.client.save_page("http://example.com", "Example", ["label1"]) + result = self.client.save_page("http://example.com", "Example", + ["label1"]) # Then self.assertIsNotNone(result) self.assertNotIn("errorCodes", result["savePage"]) @@ -117,7 +118,8 @@ def test_get_subscriptions(self): def test_archive_article(self): # Given - save_result = self.client.save_url("https://pypi.org/project/omnivorex/") + save_result = self.client.save_url( + "https://pypi.org/project/omnivorex/") self.assertIsNotNone(save_result) # When last_article = self.client.get_articles()["search"]["edges"][0] @@ -128,30 +130,32 @@ def test_archive_article(self): def test_delete_article(self): # Given - save_result = self.client.save_url("https://pypi.org/project/omnivoreql/") + save_result = self.client.save_url( + "https://pypi.org/project/omnivore_api/") self.assertIsNotNone(save_result) # When last_article = self.client.get_articles()["search"]["edges"][0] result = self.client.delete_article(last_article["node"]["id"]) # Then self.assertIsNotNone(result) - self.assertIsNotNone(result["setBookmarkArticle"]["bookmarkedArticle"]["id"]) + self.assertIsNotNone( + result["setBookmarkArticle"]["bookmarkedArticle"]["id"]) def test_create_label(self): # Given - label_input = CreateLabelInput( - name=str(hash("test_create_label")), color="#FF0000" - ) + label_input = CreateLabelInput(name=str(hash("test_create_label")), + color="#FF0000") # When result = self.client.create_label(label_input) # Then self.assertIsNotNone(result) self.assertNotIn("errorCodes", result["createLabel"]) - self.assertEqual(result["createLabel"]["label"]["name"], label_input.name) - self.assertEqual(result["createLabel"]["label"]["color"], label_input.color) - self.assertEqual( - result["createLabel"]["label"]["description"], label_input.description - ) + self.assertEqual(result["createLabel"]["label"]["name"], + label_input.name) + self.assertEqual(result["createLabel"]["label"]["color"], + label_input.color) + self.assertEqual(result["createLabel"]["label"]["description"], + label_input.description) def test_update_label(self): # Given @@ -167,17 +171,17 @@ def test_update_label(self): # Then self.assertIsNotNone(result) self.assertNotIn("errorCodes", result["updateLabel"]) - self.assertEqual(result["updateLabel"]["label"]["name"], new_label_name) + self.assertEqual(result["updateLabel"]["label"]["name"], + new_label_name) self.assertEqual(result["updateLabel"]["label"]["color"], "#0000FF") - self.assertEqual( - result["updateLabel"]["label"]["description"], "An updated TestLabel" - ) + self.assertEqual(result["updateLabel"]["label"]["description"], + "An updated TestLabel") def test_delete_label(self): # Given label_sample = self.client.create_label( - CreateLabelInput(str(hash("test_update_label")), "#FF0000") - )["createLabel"]["label"] + CreateLabelInput(str(hash("test_update_label")), + "#FF0000"))["createLabel"]["label"] # When result = self.client.delete_label(label_sample["id"]) # Then @@ -202,9 +206,8 @@ def test_set_page_labels(self): # Then self.assertIsNotNone(result) self.assertNotIn("errorCodes", result["setLabels"]) - self.assertEqual( - result["setLabels"]["labels"][0]["id"], label_sample["id"] - ) + self.assertEqual(result["setLabels"]["labels"][0]["id"], + label_sample["id"]) def test_set_page_labels_by_ids(self): # Given @@ -212,14 +215,12 @@ def test_set_page_labels_by_ids(self): label_sample = self.sample_label # When result = self.client.set_page_labels_by_ids( - page["id"], label_ids=[label_sample["id"]] - ) + page["id"], label_ids=[label_sample["id"]]) # Then self.assertIsNotNone(result) self.assertNotIn("errorCodes", result["setLabels"]) - self.assertEqual( - result["setLabels"]["labels"][0]["id"], label_sample["id"] - ) + self.assertEqual(result["setLabels"]["labels"][0]["id"], + label_sample["id"]) if __name__ == "__main__": From 91ffb0900e2c2866bdba8a98a26664cc44ef9013 Mon Sep 17 00:00:00 2001 From: Benature Date: Sun, 23 Mar 2025 00:51:22 +0800 Subject: [PATCH 06/12] feat: update_page_labels --- omnivore_api/api.py | 50 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/omnivore_api/api.py b/omnivore_api/api.py index b396b19..57f7cfd 100644 --- a/omnivore_api/api.py +++ b/omnivore_api/api.py @@ -32,6 +32,15 @@ def __init__( self.client = Client(transport=transport, fetch_schema_from_transport=False) self.queries = {} + self._username = None + + @property + def username(self) -> str: + if self._username is None: + profile = self.get_profile() + username = profile['me']['profile']['username'] + self._username = username + return self._username def _get_query(self, query_name: str) -> str: if query_name not in self.queries: @@ -115,7 +124,7 @@ def get_articles( self, limit: int = None, after: int = 0, - format: str = "html", + format: Literal['html', 'markdown'] = "html", query: str = "in:inbox", include_content: bool = False, ): @@ -305,7 +314,42 @@ def set_page_labels_by_ids(self, page_id: str, }, ) - def create_highlight(self, article_id: str, annotation: str, + def _query_label_id(self, label_name: str): + labels = getattr(self, "labels", dict()) + if label_name in labels: + return labels[label_name]["id"] + else: + refreshed_labels = self.get_labels()['labels']['labels'] + labels.update({label["name"]: label for label in refreshed_labels}) + self.labels = labels + if label_name not in labels: + raise ValueError(f"Label {label_name} not found.") + return labels[label_name]["id"] + + def update_page_labels(self, slug: str, labels: List[str] | str): + if isinstance(labels, str): + labels = [labels] + new_labels = [self._query_label_id(l) for l in labels] + + # query existed labels + article = self.get_article(self.username, slug)['article']['article'] + page_id = article['id'] + old_labels = [l['id'] for l in article['labels']] + + labels = old_labels + new_labels + labels = list(dict.fromkeys(labels)) + + return self.client.execute( + self._get_query("ApplyLabels"), + variable_values={ + "input": { + "pageId": page_id, + "labelIds": labels, + } + }, + ) + + def create_highlight(self, page_id: str, annotation: str, highlight_type: Literal["HIGHLIGHT", "NOTE"]): """ Create a new highlight. @@ -319,7 +363,7 @@ def create_highlight(self, article_id: str, annotation: str, variable_values={ "input": { "annotation": annotation, - "articleId": article_id, + "articleId": page_id, "id": str(uuid.uuid4()), "shortId": str(shortuuid.ShortUUID().random(length=8)), "type": highlight_type From 0a592b01e341beb1de03ed88240eb0ee1dc91136 Mon Sep 17 00:00:00 2001 From: Benature Date: Sun, 23 Mar 2025 00:51:46 +0800 Subject: [PATCH 07/12] ready for 0.4.0 --- omnivore_api/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/omnivore_api/__init__.py b/omnivore_api/__init__.py index 0a8d87a..5355421 100644 --- a/omnivore_api/__init__.py +++ b/omnivore_api/__init__.py @@ -1,4 +1,4 @@ from .api import OmnivoreAPI from .models import CreateLabelInput -__version__ = "0.4.0-alpha" +__version__ = "0.4.0" From 8937513653199a180b4e4867a7b53451b5a3473e Mon Sep 17 00:00:00 2001 From: Benature Date: Sun, 23 Mar 2025 00:58:50 +0800 Subject: [PATCH 08/12] fix version --- setup.py | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 4e0411a..a3f073f 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,50 @@ from setuptools import setup, find_packages import subprocess +from pathlib import Path +import ast + +PACKAGE_ENTRY = 'omnivore_api' +VERSION_FLAG = '__version__' + +with open("README.md", "r") as fh: + long_description = fh.read() + + +def get_version_from_source() -> str: + p = Path(__file__).parent / PACKAGE_ENTRY / '__init__.py' + + version_row = None + with open(str(p), 'r', encoding='utf-8') as f: + r = f.readline() + while (r): + if r.startswith(VERSION_FLAG): + version_row = r + break + + r = f.readline() + + _, version = version_row.split('=') + version = version.strip() + version = ast.literal_eval(version) + return version def get_latest_git_tag(): try: - import omnivore_api - return omnivore_api.__version__ + return get_version_from_source() + except: + pass + try: + version = subprocess.check_output( + ["git", "describe", "--tags", "--abbrev=0"]) + version = version.strip().decode( + "utf-8") # Remove trailing newline and decode bytes to string + + # Remove the 'v' from the tag + if version.startswith("v"): + version = version[1:] + + return version except Exception as e: print(f"An exception occurred while getting the latest git tag: {e}") return None @@ -30,7 +69,7 @@ def read_requirements(): author="Benature", author_email="", packages=find_packages(), - long_description=open("README.md").read(), + long_description=long_description, long_description_content_type="text/markdown", license="MIT", keywords="omnivore api readlater graphql gql client", From 6744f701f4610b1bcaffef5669b398b573de5d84 Mon Sep 17 00:00:00 2001 From: Benature Date: Sun, 23 Mar 2025 20:56:54 +0800 Subject: [PATCH 09/12] update readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 95bddeb..e0446ef 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This is a Python client for the [Omnivore API](https://omnivore.app). -[![Tests](https://github.com/Benature/OmnivoreAPI/actions/workflows/test.yml/badge.svg)](https://github.com/Benature/OmnivoreAPI/actions/workflows/test.yml) + [![PyPI version](https://badge.fury.io/py/omnivore_api.svg)](https://pypi.org/project/omnivore_api/) ## How to use @@ -59,8 +59,8 @@ omnivore.create_label(CreateLabelInput("label1", "#00ff00", "This is label descr If you find this project useful, you can support it by becoming a sponsor. Your contribution will help maintain the project and keep it up to date. -[![GitHub stars](https://img.shields.io/github/stars/Benature/omnivore_api.svg?style=social&label=Star)](https://github.com/Benature/omnivore_api/stargazers) -[![Github Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/Benature) + ## License @@ -70,4 +70,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file Check out our growth in the community: -[![Star History Chart](https://api.star-history.com/svg?repos=Benature/OmnivoreAPI&type=Date)](https://star-history.com/#Benature/OmnivoreAPI&Date) + From 3c5c8b0bd005d95a0e7efafdef44918f50a25728 Mon Sep 17 00:00:00 2001 From: BenClaw Date: Sun, 22 Feb 2026 20:03:45 +0800 Subject: [PATCH 10/12] refactor: migrate to pyproject.toml and implement CLI - Removed MANIFEST.in, requirements.txt, and setup.py in favor of pyproject.toml for package management. - Added a new CLI implementation in omnivore_api/cli.py for user interaction with the Omnivore API. - Updated README.md with installation instructions and CLI usage examples. - Introduced a Chinese version of the README (README.zh-CN.md). --- .github/workflows/test.yml | 2 +- MANIFEST.in | 8 --- README.md | 61 +++++++++++++++------ README.zh-CN.md | 102 +++++++++++++++++++++++++++++++++++ omnivore_api/__init__.py | 2 +- omnivore_api/cli.py | 85 +++++++++++++++++++++++++++++ pyproject.toml | 44 +++++++++++++++ requirements.txt | 3 -- setup.py | 88 ------------------------------ skills/omnivore-cli/SKILL.md | 76 ++++++++++++++++++++++++++ 10 files changed, 355 insertions(+), 116 deletions(-) delete mode 100644 MANIFEST.in create mode 100644 README.zh-CN.md create mode 100644 omnivore_api/cli.py create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 setup.py create mode 100644 skills/omnivore-cli/SKILL.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ff3def0..ad23ec7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + pip install -e . - name: Run unit tests run: python -m unittest discover -s tests env: diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 7abb4e4..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,8 +0,0 @@ -include MANIFEST.in -include LICENSE -include README.md -include requirements.txt -recursive-include omnivore_api * - -# exclude from sdist -exclude CONTRIBUTING.md \ No newline at end of file diff --git a/README.md b/README.md index e0446ef..fd0371c 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,59 @@ # OmnivoreAPI: Omnivore API client for Python -**Forked from OmnivoreAPI** +**[中文](README.zh-CN.md) | English** -![OmnivoreAPI Icon](https://github.com/Benature/OmnivoreAPI/assets/8194807/d51d462d-4f5a-4031-980e-1faa5ca3f6e0) +**Forked from [OmnivoreQL](https://github.com/yazdipour/OmnivoreQL)** This is a Python client for the [Omnivore API](https://omnivore.app). - [![PyPI version](https://badge.fury.io/py/omnivore_api.svg)](https://pypi.org/project/omnivore_api/) -## How to use +## Installation + +```bash +pip install omnivore_api +``` + +## CLI -To use omnivore_api in your Python project, you can follow these steps: +After installation, the `omnivore` command is available directly in your terminal. -Install the omnivore_api package using pip: +### Setup ```bash -pip install omnivore_api +omnivore init ``` -Import the package into your project and Create a new instance of the client: +Prompts for your API token and endpoint URL, then writes `~/.config/omnivore-api/config.yaml`. + +### Commands + +```bash +# Save a URL (optionally with labels) +omnivore save-url https://example.com +omnivore save-url https://example.com --label reading --label python + +# List articles in your inbox +omnivore get-articles +omnivore get-articles --limit 20 --query "in:inbox" --format markdown + +# Get your profile +omnivore get-profile + +# List all labels +omnivore get-labels +``` + +All commands output JSON to stdout, so you can pipe to `jq`: + +```bash +omnivore get-labels | jq '.[].name' +``` + +## Python API + +Import the package and create a client instance: ```python from omnivore_api import OmnivoreAPI @@ -28,8 +61,6 @@ from omnivore_api import OmnivoreAPI omnivore = OmnivoreAPI("your_api_token_here", "your_api_url_here") ``` -Use the methods of the OmnivoreAPI class to interact with the Omnivore API. - ```python profile = omnivore.get_profile() @@ -40,7 +71,7 @@ articles = omnivore.get_articles() username = profile['me']['profile']['username'] slug = articles['search']['edges'][0]['node']['slug'] -articles = omnivore.get_article(username, slug) +article = omnivore.get_article(username, slug) subscriptions = omnivore.get_subscriptions() @@ -55,9 +86,9 @@ omnivore.create_label(CreateLabelInput("label1", "#00ff00", "This is label descr * To contribute to this project: [CONTRIBUTING.md](docs/CONTRIBUTING.md) * To more know about Release process: [RELEASE.md](docs/RELEASE.md), [PYPI.md](docs/PYPI.md) -## Support + @@ -66,8 +97,8 @@ If you find this project useful, you can support it by becoming a sponsor. Your This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. -## Star History + -Check out our growth in the community: + diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..be380c9 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,102 @@ +# OmnivoreAPI:Omnivore API 的 Python 客户端 + +**中文 | [English](README.md)** + +**Forked from [OmnivoreQL](https://github.com/yazdipour/OmnivoreQL)** + +本项目是 [Omnivore API](https://omnivore.app) 的 Python 客户端。 + + +[![PyPI version](https://badge.fury.io/py/omnivore_api.svg)](https://pypi.org/project/omnivore_api/) + +## 安装 + +```bash +pip install omnivore_api +``` + +## CLI + +安装后,终端中即可使用 `omnivore` 命令。 + +### 初始化配置 + +```bash +omnivore init +``` + +按提示输入 API Token 和 Endpoint URL,配置将写入 `~/.config/omnivore-api/config.yaml`。 + +### 命令列表 + +```bash +# 保存 URL(可选添加标签) +omnivore save-url https://example.com +omnivore save-url https://example.com --label 阅读 --label python + +# 获取收件箱文章 +omnivore get-articles +omnivore get-articles --limit 20 --query "in:inbox" --format markdown + +# 获取个人信息 +omnivore get-profile + +# 获取所有标签 +omnivore get-labels +``` + +所有命令均输出 JSON 到标准输出,可配合 `jq` 使用: + +```bash +omnivore get-labels | jq '.[].name' +``` + +## Python API + +导入并创建客户端实例: + +```python +from omnivore_api import OmnivoreAPI + +omnivore = OmnivoreAPI("your_api_token_here", "your_api_url_here") +``` + +```python +profile = omnivore.get_profile() + +saved_page = omnivore.save_url("https://www.google.com") +saved_page_with_label = omnivore.save_url("https://www.google.com", ["label1", "label2"]) + +articles = omnivore.get_articles() + +username = profile['me']['profile']['username'] +slug = articles['search']['edges'][0]['node']['slug'] +article = omnivore.get_article(username, slug) + +subscriptions = omnivore.get_subscriptions() + +labels = omnivore.get_labels() +from omnivore_api import CreateLabelInput +omnivore.create_label(CreateLabelInput("label1", "#00ff00", "标签描述")) +``` + +## 文档 + +* Omnivore GraphQL Schema:[schema.graphql](https://github.com/omnivore-app/omnivore/blob/main/packages/api/src/schema.ts) +* 贡献指南:[CONTRIBUTING.md](docs/CONTRIBUTING.md) +* 发布流程:[RELEASE.md](docs/RELEASE.md)、[PYPI.md](docs/PYPI.md) + + + + + +## 许可证 + +本项目基于 MIT 许可证,详见 [LICENSE](LICENSE)。 + + + + diff --git a/omnivore_api/__init__.py b/omnivore_api/__init__.py index 5355421..66a6748 100644 --- a/omnivore_api/__init__.py +++ b/omnivore_api/__init__.py @@ -1,4 +1,4 @@ from .api import OmnivoreAPI from .models import CreateLabelInput -__version__ = "0.4.0" +__version__ = "0.5.0-beta.1" diff --git a/omnivore_api/cli.py b/omnivore_api/cli.py new file mode 100644 index 0000000..e7fca3e --- /dev/null +++ b/omnivore_api/cli.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import json +import pathlib +from typing import Annotated, Optional + +import typer +import yaml + +from . import OmnivoreAPI + +CONFIG_PATH = pathlib.Path.home() / ".config" / "omnivore-api" / "config.yaml" +DEFAULT_API_URL = "https://api-prod.omnivore.app/api/graphql" + +app = typer.Typer(help="Omnivore API CLI") + + +def _load_config() -> OmnivoreAPI: + if not CONFIG_PATH.exists(): + typer.echo(f"Config file not found: {CONFIG_PATH}", err=True) + typer.echo("Run `omnivore init` to create it.", err=True) + raise typer.Exit(code=1) + config = yaml.safe_load(CONFIG_PATH.read_text()) + api_token = config.get("api_token") + api_url = config.get("api_url", DEFAULT_API_URL) + if not api_token: + typer.echo("Missing `api_token` in config file.", err=True) + raise typer.Exit(code=1) + return OmnivoreAPI(api_token, api_url) + + +@app.command() +def init(): + """Interactive setup: create ~/.config/omnivore-api/config.yaml.""" + api_token = typer.prompt("API token") + api_url = typer.prompt("API URL", default=DEFAULT_API_URL) + CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) + CONFIG_PATH.write_text( + yaml.dump({"api_token": api_token, "api_url": api_url}, allow_unicode=True) + ) + typer.echo(f"Config written to {CONFIG_PATH}") + + +@app.command("save-url") +def save_url( + url: Annotated[str, typer.Argument(help="URL to save")], + label: Annotated[Optional[list[str]], typer.Option(help="Label to apply (repeatable)")] = None, +): + """Save a URL to Omnivore.""" + client = _load_config() + result = client.save_url(url, labels=label or []) + typer.echo(json.dumps(result, indent=2, ensure_ascii=False)) + + +@app.command("get-articles") +def get_articles( + limit: Annotated[Optional[int], typer.Option(help="Max number of articles to return")] = None, + after: Annotated[int, typer.Option(help="Cursor offset")] = 0, + query: Annotated[str, typer.Option(help="Search query")] = "in:inbox", + format: Annotated[str, typer.Option(help="Output format: html or markdown")] = "html", +): + """List articles from Omnivore.""" + client = _load_config() + result = client.get_articles(limit=limit, after=after, query=query, format=format) + typer.echo(json.dumps(result, indent=2, ensure_ascii=False)) + + +@app.command("get-profile") +def get_profile(): + """Get the current user's profile.""" + client = _load_config() + result = client.get_profile() + typer.echo(json.dumps(result, indent=2, ensure_ascii=False)) + + +@app.command("get-labels") +def get_labels(): + """List all labels for the current user.""" + client = _load_config() + result = client.get_labels() + typer.echo(json.dumps(result, indent=2, ensure_ascii=False)) + + +def main(): + app() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..baeb06b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[project] +name = "omnivore_api" +dynamic = ["version"] +description = "Omnivore API Client for Python" +readme = "README.md" +license = { text = "MIT" } +authors = [{ name = "Benature" }] +keywords = ["omnivore", "api", "readlater", "graphql", "gql", "client"] +requires-python = ">=3.9" +classifiers = [ + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries", + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", +] +dependencies = [ + "python-dotenv", + "gql>=3.5.0", + "requests-toolbelt>=1.0.0", + "shortuuid", + "typer", + "pyyaml", +] + +[project.urls] +Homepage = "https://github.com/Benature/OmnivoreAPI" +"Bug Tracker" = "https://github.com/Benature/OmnivoreAPI/issues" +"Source Code" = "https://github.com/Benature/OmnivoreAPI" + +[project.scripts] +omnivore = "omnivore_api.cli:main" + +[tool.setuptools.dynamic] +version = { attr = "omnivore_api.__version__" } + +[tool.setuptools.packages.find] +where = ["."] + +[tool.setuptools.package-data] +omnivore_api = ["queries/*.graphql", "queries/*.graphqls"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 9c60e9f..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -python-dotenv -gql>=3.5.0 -requests-toolbelt>=1.0.0 \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index a3f073f..0000000 --- a/setup.py +++ /dev/null @@ -1,88 +0,0 @@ -from setuptools import setup, find_packages -import subprocess -from pathlib import Path -import ast - -PACKAGE_ENTRY = 'omnivore_api' -VERSION_FLAG = '__version__' - -with open("README.md", "r") as fh: - long_description = fh.read() - - -def get_version_from_source() -> str: - p = Path(__file__).parent / PACKAGE_ENTRY / '__init__.py' - - version_row = None - with open(str(p), 'r', encoding='utf-8') as f: - r = f.readline() - while (r): - if r.startswith(VERSION_FLAG): - version_row = r - break - - r = f.readline() - - _, version = version_row.split('=') - version = version.strip() - version = ast.literal_eval(version) - return version - - -def get_latest_git_tag(): - try: - return get_version_from_source() - except: - pass - try: - version = subprocess.check_output( - ["git", "describe", "--tags", "--abbrev=0"]) - version = version.strip().decode( - "utf-8") # Remove trailing newline and decode bytes to string - - # Remove the 'v' from the tag - if version.startswith("v"): - version = version[1:] - - return version - except Exception as e: - print(f"An exception occurred while getting the latest git tag: {e}") - return None - - -def read_requirements(): - with open("requirements.txt") as f: - return f.read().splitlines() - - -VERSION = get_latest_git_tag() or "0.0.1" # Fallback version - -PROJECT_URLS = { - "Bug Tracker": "https://github.com/Benature/OmnivoreAPI/issues", - "Source Code": "https://github.com/Benature/OmnivoreAPI", -} - -setup( - name="omnivore_api", - version=VERSION, - description="Omnivore API Client for Python", - author="Benature", - author_email="", - packages=find_packages(), - long_description=long_description, - long_description_content_type="text/markdown", - license="MIT", - keywords="omnivore api readlater graphql gql client", - platforms="any", - url="https://github.com/Benature/OmnivoreAPI", - project_urls=PROJECT_URLS, - include_package_data=True, - python_requires=">=3", - classifiers=[ - "Intended Audience :: Developers", - "Topic :: Software Development :: Libraries", - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - ], - install_requires=read_requirements(), -) diff --git a/skills/omnivore-cli/SKILL.md b/skills/omnivore-cli/SKILL.md new file mode 100644 index 0000000..240daca --- /dev/null +++ b/skills/omnivore-cli/SKILL.md @@ -0,0 +1,76 @@ +--- +name: omnivore-cli +description: Interact with the Omnivore read-later service via the `omnivore` CLI. Covers setup, saving URLs, fetching articles/profile/labels, extending the CLI with new commands, and modifying omnivore_api/cli.py. Use when the user asks to use, test, or extend the omnivore CLI tool. +--- + +# Omnivore CLI + +## Overview + +The `omnivore` CLI is a typer-based command-line tool provided by the `omnivore_api` Python package. + +Entry point: `omnivore_api/cli.py` → registered as `omnivore` via `[project.scripts]` in `pyproject.toml`. + +## First-time Setup + +```bash +omnivore init +# prompts for API token and API URL +# writes to ~/.config/omnivore-api/config.yaml +``` + +Config file format (`~/.config/omnivore-api/config.yaml`): + +```yaml +api_token: "your-token-here" +api_url: "https://api-prod.omnivore.app/api/graphql" +``` + +## Available Commands + +| Command | Description | Key options | +|---------|-------------|-------------| +| `omnivore init` | Interactive config setup | — | +| `omnivore save-url ` | Save a URL | `--label` (repeatable) | +| `omnivore get-articles` | List inbox articles | `--limit`, `--after`, `--query`, `--format` | +| `omnivore get-profile` | Current user info | — | +| `omnivore get-labels` | All labels | — | + +All read commands output **JSON** to stdout. + +## Usage Examples + +```bash +# Save with labels +omnivore save-url https://example.com --label reading --label python + +# Paginated fetch, markdown format +omnivore get-articles --limit 20 --after 20 --format markdown + +# Filter by date range (Omnivore search syntax) +omnivore get-articles --query "in:inbox published:2024-01-01..*" + +# Pipe to jq +omnivore get-labels | jq '.[].name' +``` + +## Adding a New CLI Command + +1. Open `omnivore_api/cli.py` +2. Add a new function decorated with `@app.command("command-name")` +3. Use `Annotated` + `typer.Option` / `typer.Argument` for parameters +4. Call the corresponding `OmnivoreAPI` method via `_load_config()` +5. Output with `typer.echo(json.dumps(result, indent=2))` + +Example skeleton: + +```python +@app.command("delete-article") +def delete_article( + article_id: Annotated[str, typer.Argument(help="Article ID")], +): + """Delete an article from Omnivore.""" + client = _load_config() + result = client.delete_article(article_id) + typer.echo(json.dumps(result, indent=2)) +``` From af3b496fc56dbc3ac169205e1e8b223da1fa236a Mon Sep 17 00:00:00 2001 From: BenClaw Date: Sun, 22 Feb 2026 20:08:02 +0800 Subject: [PATCH 11/12] chore: add future annotations for improved type hinting --- omnivore_api/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/omnivore_api/api.py b/omnivore_api/api.py index 57f7cfd..a56a352 100644 --- a/omnivore_api/api.py +++ b/omnivore_api/api.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import uuid import shortuuid import os From 7724cffd8106caaa468e1f77e42a8c763f5e3157 Mon Sep 17 00:00:00 2001 From: BenClaw Date: Sun, 22 Feb 2026 22:20:49 +0800 Subject: [PATCH 12/12] fix: set default GraphQL endpoint in OmnivoreAPI constructor --- omnivore_api/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/omnivore_api/api.py b/omnivore_api/api.py index a56a352..3d064f4 100644 --- a/omnivore_api/api.py +++ b/omnivore_api/api.py @@ -12,10 +12,12 @@ class OmnivoreAPI: + DEFAULT_GRAPHQL_ENDPOINT = "https://api-prod.omnivore.app/api/graphql" + def __init__( self, api_token: str, - graphql_endpoint_url: str, + graphql_endpoint_url: str = DEFAULT_GRAPHQL_ENDPOINT, ) -> None: """ Initialize a new instance of the GraphQL client.