yazdipour / OmnivoreQL

@Omnivore-app API client for Python
https://pypi.org/project/omnivoreql/
MIT License
69 stars 6 forks source link

Feature request: add/edit/remove labels of article #16

Closed thiswillbeyourgithub closed 5 months ago

thiswillbeyourgithub commented 5 months ago

Hi,

I'm coding something to automatically optimize my reading queue based on ELO scores : https://github.com/thiswillbeyourgithub/mini_LiTOY/blob/main/examples/omnivore_litoy.py

But I'm currently stuck because I don't know how I can use the API to modify labels of an article. I also asked on omnivore repo.

Is that something that could be possible? Or could you tell me how I could use for example a simple python request?

Thanks!

yazdipour commented 5 months ago

Currently the feature is not implemented in the python lib. But its a great feature to have. I can see there are already mutations in graphql schema of omnivore, so it should be possible. https://github.com/yazdipour/OmnivoreQL/blob/440101289122d42c4172b165b8667f9fb3ac5c84/omnivoreql/schema.gql#L2527-L2530

thiswillbeyourgithub commented 5 months ago

Thanks! I'm not familiar with graphQL, so does that mean I can use a request right away? For example gpt4 suggests this. Does that make sense?

import requests

url = 'https://your-graphql-endpoint.com/graphql'
headers = {'Content-Type': 'application/json'}

def create_label(name):
    query = '''
    mutation {
        createLabel(input: {name: "%s"}) {
            label {
                id
                name
            }
        }
    }
    ''' % name
    response = requests.post(url, json={'query': query}, headers=headers)
    return response.json()

def update_label(id, new_name):
    query = '''
    mutation {
        updateLabel(input: {id: "%s", name: "%s"}) {
            label {
                id
                name
            }
        }
    }
    ''' % (id, new_name)
    response = requests.post(url, json={'query': query}, headers=headers)
    return response.json()

def delete_label(id):
    query = '''
    mutation {
        deleteLabel(id: "%s") {
            success
        }
    }
    ''' % id
    response = requests.post(url, json={'query': query}, headers=headers)
    return response.json()

def set_labels(ids):
    query = '''
    mutation {
        setLabels(input: {ids: [%s]}) {
            success
        }
    }
    ''' % ','.join(f'"{id}"' for id in ids)
    response = requests.post(url, json={'query': query}, headers=headers)
    return response.json()
yazdipour commented 5 months ago

well simple answer is to just try it! But my suggestion is to clone this repo, and use https://github.com/yazdipour/OmnivoreQL/blob/main/omnivoreql/omnivoreql.py as example and add you new methods that follows the same sorta structure!

thiswillbeyourgithub commented 5 months ago

Thank. So I tried and failed.

I notice that your schema seems to be not up to date anymore? https://github.com/omnivore-app/omnivore/blob/main/packages/api/src/schema.ts is linked from https://docs.omnivore.app/integrations/api.html

Anyhow I don't know anything about graphQL, only python so just in case here's what i tried:

I have the following code taken from graphql related files:

input CreateLabelInput {
  name: String! @sanitize(maxLength: 64)
  color: String @sanitize(pattern: "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")
  description: String @sanitize(maxLength: 100)
}
input UpdateLabelInput {
  labelId: ID!
  color: String!
  description: String
  name: String!
}
input SetLabelsInput {
  pageId: ID!
  labelIds: [ID!]
  labels: [CreateLabelInput!]
  source: String
}

createLabel(input: CreateLabelInput!): CreateLabelResult!
updateLabel(input: UpdateLabelInput!): UpdateLabelResult!
setLabels(input: SetLabelsInput!): SetLabelsResult!

Here are a few reference method:

    def save_url(self, url: str):
        """
        Save a URL to Omnivore.

        :param url: The URL to save.
        """
        mutation = gql(
            """
            mutation {
                saveUrl(input: { clientRequestId: "%s", source: "api", url: "%s" }) {
                    ... on SaveSuccess {
                        url
                        clientRequestId
                    }
                    ... on SaveError {
                        errorCodes
                        message
                    }
                }
            }
        """
            % (uuid.uuid4(), url)
        )
        return self.client.execute(mutation)

    def save_page(self, url: str, original_content: str):
        """
        Save a page with html content to Omnivore.

        :param url: The URL of the page to save.
        :param original_content: The original html content of the page.
        """
        mutation = gql(
            """
            mutation {
                savePage(input: { clientRequestId: "%s", source: "api", url: "%s", originalContent:"%s" }) {
                    ... on SaveSuccess {
                        url
                        clientRequestId
                    }
                    ... on SaveError {
                        errorCodes
                        message
                    }
                }
            }
        """
            % (uuid.uuid4(), url, original_content)
        )
        return self.client.execute(mutation)

I want you to write the methods needed to manage the labels

To which GPT-4o replied:

    def create_label(self, name: str, color: str = None, description: str = None):
        mutation = gql(
            """
            mutation {
                createLabel(input: { 
                    name: "%s", 
                    color: "%s", 
                    description: "%s" 
                }) {
                    ... on CreateLabelResult {
                        label {
                            id
                            name
                            color
                            description
                        }
                    }
                    ... on CreateLabelError {
                        errorCodes
                        message
                    }
                }
            }
            """ % (name, color, description)
        )
        return self.client.execute(mutation)

    def update_label(self, label_id: str, name: str, color: str = None, description: str = None):
        mutation = gql(
            """
            mutation {
                updateLabel(input: { 
                    labelId: "%s", 
                    name: "%s", 
                    color: "%s", 
                    description: "%s" 
                }) {
                    ... on UpdateLabelResult {
                        label {
                            id
                            name
                            color
                            description
                        }
                    }
                    ... on UpdateLabelError {
                        errorCodes
                        message
                    }
                }
            }
            """ % (label_id, name, color, description)
        )
        return self.client.execute(mutation)

    def set_labels(self, page_id: str, label_ids: list, labels: list, source: str = "api"):
        label_inputs = ", ".join([
            '{ name: "%s", color: "%s", description: "%s" }' % (label['name'], label.get('color', ''), label.get('description', ''))
            for label in labels
        ])
        mutation = gql(
            """
            mutation {
                setLabels(input: { 
                    pageId: "%s", 
                    labelIds: [%s], 
                    labels: [%s], 
                    source: "%s" 
                }) {
                    ... on SetLabelsResult {
                        success
                    }
                    ... on SetLabelsError {
                        errorCodes
                        message
                    }
                }
            }
            """ % (page_id, ', '.join(label_ids), label_inputs, source)
        )
        return self.client.execute(mutation)

But no matter what I try I seem to get gql.transport.exceptions.TransportQueryError: {'message': 'Unexpected server error'} which is not at all informative!

Not knowing anything about graphQL and being super short on time, I don't have time to investigate more sadly.

thiswillbeyourgithub commented 5 months ago

update:

I dug up the android mutation used:

I think a part of my issue is that an argument "pageId" seems to be required even though all my article have "pageId" set to None apparently. Or maybe it's a parsing issue on omnivoreql's side?

Also I can't for the life of me understand what kind of arguments should go as "label_ids" and "label_inputs" even though label_inputs is a list of dict so could completely contain the label ids and not doing so could lead to unexpected errors.

yazdipour commented 5 months ago

Hey. This weekend i had a bit of time to work on this. And i added some new features like createLabel, updateLabel, deleteLabel. But i am still struggling with setLabel and its WIP. Regarding pageid: server return clientRequestId as pageid if i am not mistaken.

thiswillbeyourgithub commented 5 months ago

Thanks a lot!

yazdipour commented 5 months ago

You are welcome. Btw you can now set your labels while using save_url.

And for the setlabel, pull request is in here but still wip. https://github.com/yazdipour/OmnivoreQL/issues/21

thiswillbeyourgithub commented 5 months ago

Just to be sure: that means I can currently decide what labels to apply when saving a url, but I can't modify the label of an article I already saved in the past. Correct?

yazdipour commented 5 months ago

Yes.

yazdipour commented 5 months ago

@thiswillbeyourgithub set_page_labels, set_page_labels_by_ids are now added in v0.3.3

thiswillbeyourgithub commented 5 months ago

Thank you so much! I'll finally be able to move forward my project now!