googleapis / google-api-python-client

🐍 The official Python client library for Google's discovery based APIs.
https://googleapis.github.io/google-api-python-client/docs/
Apache License 2.0
7.78k stars 2.42k forks source link

Photos mediaItems.search doesn't accept correct parameters #1764

Open alexhayes opened 2 years ago

alexhayes commented 2 years ago

TL/DR This is not a bug - see my comment.


When attempting to call the search method from the mediaItems resource for Google Photos API with any of the allowed parameters an exception is thrown as follows;

TypeError: Got an unexpected keyword argument albumId

I suspect this has something to do with the discovery URL which, for the method in question, does not define any parameters.

image

As shown, it does define a request with a $ref of SearchMediaItemsRequest, which in the discovery URL is as follows;

image

As far as I can ascertain however google-api-python-client doesn't appear to reference $ref anywhere, so I am assuming it takes the method signature only from parameters defined in the discovery URL.

Oddly enough however, when the Resource is created, there are some parameters set for the search method, but these appear to be derived from somewhere else (perhaps oath2.v2.json???) See below;

EDIT: It would appear these are set by _fix_up_parameters in discovery.py which happens to all methods.

PyCharm debug showing the incorrect parameters attached to the methodDesc

Given it appears the parameters in the discovery URL are incorrect, I'm unsure if this is a bug in google-api-python-client - if not, please direct me to where I should raise this.

NOTE: Calls without any parameters return the expected result (ie. media items).

Environment details

Steps to reproduce

  1. Retrieve a Resource with serviceName='photoslibrary', version='v1'
  2. Attempt to call service.mediaItems().search(albumId="xyz").execute()

Code example

import os

import typing

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build, Resource

def create_credentials(secrets_path: str, scopes: typing.List[str]) -> Credentials:
    path = os.path.splitext(secrets_path)[0]
    tokens_path = f'{path}-cached-token.json'

    if os.path.exists(tokens_path):
        credentials = Credentials.from_authorized_user_file(tokens_path, scopes)
    else:
        credentials = None

    # If there are no (valid) credentials available, let the user log in.
    if not credentials or not credentials.valid:
        if credentials and credentials.expired and credentials.refresh_token:
            credentials.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(secrets_path, scopes)
            credentials = flow.run_local_server(port=0)

        # Save the credentials for the next run
        with open(tokens_path, 'w') as token:
            token.write(credentials.to_json())

    return credentials

def create_photos_service(secrets_path: str) -> Resource:
    credentials = create_credentials(
        secrets_path,
        [
            'https://www.googleapis.com/auth/photoslibrary'
        ]
    )

    return build('photoslibrary', 'v1', credentials=credentials, static_discovery=False)

service = create_photos_service("client_secret.json")
media_items = service.mediaItems().search(albumId="xyz").execute()

Stack trace

/path/to/.direnv/python-3.10.0/bin/python /path/to/helpers.py
Traceback (most recent call last):
  File "/path/to/helpers.py", line 59, in <module>
    media_items = service.mediaItems().search(pageSize="xyz").execute()
  File "/path/to/.direnv/python-3.10.0/lib/python3.10/site-packages/googleapiclient/discovery.py", line 1019, in method
    raise TypeError('Got an unexpected keyword argument {}'.format(name))
TypeError: Got an unexpected keyword argument albumId

Process finished with exit code 1
alexhayes commented 2 years ago

This can be closed as INVALID - it would appear that for methods that are HTTP POST you need to provide a body attribute with the values in it - as follows;

media_items = service.mediaItems().search(body=dict(albumId="xyz")).execute()

That seems somewhat unintuitive to me and I'm not able to find that documented - I just found it by chance as I was digging around StackOverflow (and yes, the issue template says "check SO", I did, but didn't find it initially).

As a consumer of an API, do I really care if it's a POST, GET, PATCH etc..? As an engineer I definitely want to know what happens under the hood, but it feels a bit off to be exposed to that by the interface signature. My assumption is there are good reasons.

If someone would like me to, I'm happy to document this via a PR if there is a particular area of the docs someone thinks is the best place to put it.

parthea commented 2 years ago

Hi @alexhayes,

I agree this information can be difficult to find. In case you're still interested in updating the docs, PRs are welcome !

Victorivus commented 11 months ago

This can be closed as INVALID - it would appear that for methods that are HTTP POST you need to provide a body attribute with the values in it - as follows;

media_items = service.mediaItems().search(body=dict(albumId="xyz")).execute()

That seems somewhat unintuitive to me and I'm not able to find that documented - I just found it by chance as I was digging around StackOverflow (and yes, the issue template says "check SO", I did, but didn't find it initially).

As a consumer of an API, do I really care if it's a POST, GET, PATCH etc..? As an engineer I definitely want to know what happens under the hood, but it feels a bit off to be exposed to that by the interface signature. My assumption is there are good reasons.

If someone would like me to, I'm happy to document this via a PR if there is a particular area of the docs someone thinks is the best place to put it.

I spent hours testing and surfing the internet until I found your comment, thanks !