OAuthlib support for Python-Requests!

Overview

Requests-OAuthlib build-status coverage-status Documentation Status

This project provides first-class OAuth library support for Requests.

The OAuth 1 workflow

OAuth 1 can seem overly complicated and it sure has its quirks. Luckily, requests_oauthlib hides most of these and let you focus at the task at hand.

Accessing protected resources using requests_oauthlib is as simple as:

>>> from requests_oauthlib import OAuth1Session
>>> twitter = OAuth1Session('client_key',
                            client_secret='client_secret',
                            resource_owner_key='resource_owner_key',
                            resource_owner_secret='resource_owner_secret')
>>> url = 'https://api.twitter.com/1/account/settings.json'
>>> r = twitter.get(url)

Before accessing resources you will need to obtain a few credentials from your provider (e.g. Twitter) and authorization from the user for whom you wish to retrieve resources for. You can read all about this in the full OAuth 1 workflow guide on RTD.

The OAuth 2 workflow

OAuth 2 is generally simpler than OAuth 1 but comes in more flavours. The most common being the Authorization Code Grant, also known as the WebApplication flow.

Fetching a protected resource after obtaining an access token can be extremely simple. However, before accessing resources you will need to obtain a few credentials from your provider (e.g. Google) and authorization from the user for whom you wish to retrieve resources for. You can read all about this in the full OAuth 2 workflow guide on RTD.

Installation

To install requests and requests_oauthlib you can use pip:

$ pip install requests requests_oauthlib
Comments
  • The URL should always be native.

    The URL should always be native.

    This should resolve the issue that @michaelhelmick had in kennethreitz/requests#1366. In short, the keys for doing Transport Adapter lookups are native strings, but requests_oauthlib turns the URL into bytes. Not helpful. This prevents that behaviour while avoiding regressing kennethreitz/requests#1252.

    opened by Lukasa 50
  • Refresh Tokens + Web App Example?

    Refresh Tokens + Web App Example?

    I've been trying to wrap my head around refresh tokens and the OAuth2 web example (https://requests-oauthlib.readthedocs.org/en/latest/examples/real_world_example.html). Would it be possible to provide an example usage of refresh tokens in that context? Where in the code it would go, etc?

    Just a suggestion! :grin:

    docs 
    opened by bryanveloso 24
  • Fix dependencies

    Fix dependencies

    Each time the oauthlib library is changed, I have to change my code because the behaviour changes. So, can you fix the version of the libraries that you use? At least you should fix the mayor number of the dependencies since when this number changes, the API often changes.

    opened by aitormagan 23
  • AttributeError: 'PreparedRequest' object has no attribute 'data'

    AttributeError: 'PreparedRequest' object has no attribute 'data'

    @Lukasa asked me to raise this error in this repo as well:

    Hey guys, just wanted to bring this issue forward. With requests 1.0.0 So the error came from when I went to test Twython with the new requests release.

    And because it bothered me, I removed self from all variables that used self like self.callback_url Also, I striped the code out of the actual function get_authentication_tokens (so that's why you'll see ref to that function in the traceback)

    import requests
    from requests_oauthlib import OAuth1
    
    app_key = u'SUPERDUPERSECRETKEY'
    app_secret = u'SUPERDUPERSECRETSECRET'
    
    callback_url = 'http://example.com'
    headers = {'User-Agent': 'Twython v2.5.5'}
    auth = OAuth1(app_key, app_secret,
                                   signature_type='auth_header')
    
    request_args['oauth_callback'] = callback_url
    response = requests.get('https://api.twitter.com/oauth/request_token', params=request_args, headers=headers, auth=auth)
    
    if response.status_code != 200:
        raise TwythonAuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (response.status_code, response.content))
    
    request_tokens = dict(parse_qsl(response.content))
    if not request_tokens:
        raise TwythonError('Unable to decode request tokens.')
    
    oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed') == 'true'
    
    auth_url_params = {
        'oauth_token': request_tokens['oauth_token'],
    }
    
    # Use old-style callback argument if server didn't accept new-style
    if callback_url and not oauth_callback_confirmed:
        auth_url_params['oauth_callback'] = callback_url
    
    request_tokens['auth_url'] = 'https://api.twitter.com/oauth/authenticate?' + urllib.urlencode(auth_url_params)
    
    return request_tokens
    

    Anyways, when I try to run this code I get the error:

    Traceback (most recent call last):
      File "twython.py", line 585, in <module>
        auth_props = t.get_authentication_tokens()
      File "twython.py", line 271, in get_authentication_tokens
        response = requests.get('https://api.twitter.com/oauth/request_token', params=request_args, headers=headers, auth=self.auth)
      File "/Users/mikehelmick/.virtualenv/twython/lib/python2.7/site-packages/requests/api.py", line 49, in get
        return request('get', url, **kwargs)
      File "/Users/mikehelmick/.virtualenv/twython/lib/python2.7/site-packages/requests/api.py", line 38, in request
        return session.request(method=method, url=url, **kwargs)
      File "/Users/mikehelmick/.virtualenv/twython/lib/python2.7/site-packages/requests/sessions.py", line 253, in request
        prep = req.prepare()
      File "/Users/mikehelmick/.virtualenv/twython/lib/python2.7/site-packages/requests/models.py", line 200, in prepare
        p.prepare_auth(self.auth)
      File "/Users/mikehelmick/.virtualenv/twython/lib/python2.7/site-packages/requests/models.py", line 336, in prepare_auth
        r = auth(self)
      File "/Users/mikehelmick/.virtualenv/twython/lib/python2.7/site-packages/requests_oauthlib/core.py", line 41, in __call__
        decoded_body = extract_params(r.data)
    AttributeError: 'PreparedRequest' object has no attribute 'data'
    
    opened by michaelhelmick 20
  • Sort out the documentation

    Sort out the documentation

    We've finally got some stuff up on ReadTheDocs, but it's not great. This issue is an umbrella tracking issue, so I can tick stuff off as we go. Here are some things we need to do: feel free to suggest more in the comments.

    • [ ] Sort out the README (partly in #46).
    • [x] Sort out Intersphinx (see #47).
    • [ ] Remove lengthy worked examples from the API docs.
    • [ ] Put lengthy worked examples somewhere else (see #46).
    • [ ] Improve clarity of API docs.
    • [ ] Improve landing page.
    • [ ] Tutorials. Lots of tutorials. A whole tutorials section (see #49).
    enhancement Contributor Friendly docs 
    opened by Lukasa 19
  • Forcing HTTPBasicAuth in fetch_token results in invalid_request from Google

    Forcing HTTPBasicAuth in fetch_token results in invalid_request from Google

    Using Flask_OAuth2_Login (for example):

      def login(self):
        sess = self.session()
    
        # Get token
        try:
          sess.fetch_token(
            self.token_url,
            code=request.args["code"],
            client_secret=self.client_secret,
          )
    

    results in:

      File "/lib/python2.7/site-packages/flask_oauth2_login/base.py", line 56, in login
        client_secret=self.client_secret,
      File "/lib/python2.7/site-packages/requests_oauthlib/oauth2_session.py", line 232, in fetch_token
        self._client.parse_request_body_response(r.text, scope=self.scope)
      File "/lib/python2.7/site-packages/oauthlib/oauth2/rfc6749/clients/base.py", line 409, in parse_request_body_response
        self.token = parse_token_response(body, scope=scope)
      File "/lib/python2.7/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 376, in parse_token_response
        validate_token_parameters(params)
      File "/lib/python2.7/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 383, in validate_token_parameters
        raise_from_error(params.get('error'), params)
      File "/lib/python2.7/site-packages/oauthlib/oauth2/rfc6749/errors.py", line 271, in raise_from_error
        raise cls(**kwargs)
    

    This is due to a change introduced in 0.6.0 in oauth2_session.py/fetch_token:

    auth = auth or requests.auth.HTTPBasicAuth(username, password)
    

    whereas previously auth was allowed to remain empty. Google responds with:

    {
      "error" : "invalid_request"
    }
    

    and everything falls down from there. Commenting out the line allows the request to complete normally.

    opened by butlertron 17
  • Only pass bytes to urllib3.

    Only pass bytes to urllib3.

    This should resolve requests-oauthlib's problems with uploading binary data, as demonstrated in kennethreitz/requests#1252.

    @sigmavirus24, can I get code review on this before I merge into master?

    opened by Lukasa 17
  • OAuth2Session(client_id=client_id, client=client) return 403 error in production environment

    OAuth2Session(client_id=client_id, client=client) return 403 error in production environment

    OAuth2Session(client_id=client_id, client=client) return 403 error in production environment. Works good in localhost

    In localhost and production environment I disabled OAUTHLIB_INSECURE_TRANSPORT os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'

    opened by ilGuccio 14
  • Fix spaces encoding in parameters

    Fix spaces encoding in parameters

    OAuth requires spaces to be encoded as %20 instead of + in order to generate a valid signature.

    Explained in http://troy.yort.com/2-common-problems-with-oauth-client-libraries/

    Before this patch, I was unable to use Yahoo!'s BOSS Placefinder API, which now seems to be responding correctly to me, instead of what it used to say:

    <?xml version='1.0' encoding='UTF-8'?>
    <yahoo:error xmlns:yahoo='http://yahooapis.com/v1/base.rng'
      xml:lang='en-US'>
      <yahoo:description>Please provide valid credentials. OAuth oauth_problem="signature_invalid", realm="yahooapis.com"</yahoo:description>
    </yahoo:error>
    
    opened by Xowap 14
  • Tidying up a bit for requests 1.0

    Tidying up a bit for requests 1.0

    I've added some initial tests and cleaned up the extension code a bit.

    Unfortunately this code depends heavily on an update to requests.models.py in which lines 200 & 201 are swapped, i.e.

        p.prepare_auth(self.auth)
        p.prepare_body(self.data, self.files)
    

    becomes

        p.prepare_body(self.data, self.files)
        p.prepare_auth(self.auth)
    

    Why? Because we have no idea in auth whether body will soon be filled with files data or not and consequently it would be foolish to assume form encoded on empty body.

    I've not had time to look into what implications this might have for requests. Will send a PR when I have. @kennethreitz

    opened by ib-lundgren 14
  • Prepare 0.4.0 release

    Prepare 0.4.0 release

    The current 0.3.0 release is pretty old and we've fixed a few bugs, as well as added a massive amount of functionality. I want to get this library into a shape where we can pass it to Kenneth all he has to do is tag it and go. @ib-lundgren, @sigmavirus24, is there anything we need to do?

    opened by Lukasa 13
  • Scope changes with Microsoft services & `offline_access`

    Scope changes with Microsoft services & `offline_access`

    I'm trying to set up OAuth2 for unattended access to Microsoft IMAP servers - the refresh_token is important here.

    When providing a request scope set as follows:

    • offline_access
    • https://outlook.office.com/User.Read
    • https://outlook.office.com/IMAP.AccessAsUser.All

    The service responds with the following (i.e: offline_access is removed):

    • https://outlook.office.com/User.Read
    • https://outlook.office.com/IMAP.AccessAsUser.All

    This results in a warning being raised.

    Traceback
    Traceback (most recent call last):
      File "./oauth2-test.py", line 51, in <module>
        token = oauth.fetch_token(token_url, client_secret=client_secret, authorization_response=redirect_response)
      File "/usr/lib/python3.8/site-packages/requests_oauthlib/oauth2_session.py", line 366, in fetch_token
        self._client.parse_request_body_response(r.text, scope=self.scope)
      File "/usr/lib/python3.8/site-packages/oauthlib/oauth2/rfc6749/clients/base.py", line 427, in parse_request_body_response
        self.token = parse_token_response(body, scope=scope)
      File "/usr/lib/python3.8/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 441, in parse_token_response
        validate_token_parameters(params)
      File "/usr/lib/python3.8/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 471, in validate_token_parameters
        raise w
    Warning: Scope has changed from "https://outlook.office.com/User.Read https://outlook.office.com/IMAP.AccessAsUser.All offline_access" to "https://outlook.office.com/User.Read https://outlook.office.com/IMAP.AccessAsUser.All".
    

    Apparently the offline_access scope should never be returned by Microsoft services, as it's not actually a useful scope for accessing resources (ref).


    My current approach (which isn't ideal), is as follows:

    oauth = OAuth2Session(client_id, redirect_uri=redirect_uri, scope=scope)
    authorization_url, state = oauth.authorization_url(authorize_url)
    
    # remove the `offline_access` scope directly / by hand
    oauth.scope.remove('offline_access')
    
    # ... submit the request to authorization_url, and retrieve the token
    redirect_response = ...
    token = oauth.fetch_token(token_url, client_secret=client_secret, authorization_response=redirect_response)
    

    I'm aware of OAUTHLIB_RELAX_TOKEN_SCOPE (link), but that seems perhaps a little over-permissive.

    Perhaps one of the following would be a good idea?

    • A more generic mechanism to permit accepting scope changes
    • A way to supply the expected response set of scopes
    • A list of "I don't mind if these aren't grated" scopes
    opened by attie-argentum 0
  • Requirements out of date

    Requirements out of date

    The version of requests pinned by this library is 2.26.0.

    requests==2.26.0

    Currently, requests is at version 2.28.1. Please consider updating the requirements or leaving the requests version unpinned, as it is in requirements.in.

    Thanks!

    opened by MooseV2 1
  • Add refresh token exception hook to list of compliance hooks?

    Add refresh token exception hook to list of compliance hooks?

    I have a use case were sometimes my stored users' tokens expire and thus they need to reauthenticate into whatever app I'm trying to request data from.

    In these cases I get an error 400 from the api I'm requesting from and simply ask my user to reauthenticate using my usual oAuth2 flow.

    Since I don't know exactly when a token will expire my current approach is to wrap every request I make to these apis in a try catch and then handle the exception from there.

    I noticed that there are some compliance hooks and was wondering if it would be appropriate to add something like a refresh_token_exception hook to be called if the library runs into an error code while refreshing the token as opposed to just calling the usual refresh_token_response with the raw response.

    If this makes sense I would be happy to submit a pull request for it

    https://github.com/requests/requests-oauthlib/blob/3a2a852e33c691c7e793300ce366a01b6e4b3848/requests_oauthlib/oauth2_session.py#L94

    opened by ramennoodles20 0
  • `oauth2_session.OAuth2Session.refresh_token` creates infinite loop with Exchange Online when token expires

    `oauth2_session.OAuth2Session.refresh_token` creates infinite loop with Exchange Online when token expires

    Using an expired access token with Microsoft Exchange Online for sending Outlook e-mails under exchangelib runs into an infinite loop during the token refresh just because the app creds are bundled into the body structure instead of being provided as a (client_id, client_secret) pair given the auth object. (context)

    Is it possible to automatically create and use an auth object (if it isn't provided explicitly by the caller) based on these refresh kwargs (if they contain the client creds) right inside of oauth2_session.OAuth2Session.refresh_token so the /token POST will benefit from this implicit auth object and make the server auth working with Exchange?

    opened by cmin764 0
  • WIP: Add PKCE support with oauthlib 3.2.0

    WIP: Add PKCE support with oauthlib 3.2.0

    Since oauthlib 3.2.0 now supports PKCE for Clients (https://github.com/oauthlib/oauthlib/releases/tag/v3.2.0), this PR proposes a first implementation . Any feedbacks are welcome, I'm not sure it is production ready yet.

    Change from: session = OAuth2Session(client_id) to session = OAuth2Session(app.client_id, pkce="S256")

    And be sure to reuse the same session for fetch_token, as it will need to remember code_verifier. It is not really practical beyond PoC, so any suggestions are welcome.

    opened by JonathanHuot 0
Releases(v1.3.1)
  • v1.3.1(Jan 29, 2022)

    What's Changed

    • Add Support for OAuth Mutual TLS (draft-ietf-oauth-mtls) by @danielfett in https://github.com/requests/requests-oauthlib/pull/389
    • Linkedin compliance removal & LinkedIn Example update/fix by @jtroussard in https://github.com/requests/requests-oauthlib/pull/397
    • docs: Fix typos in token refresh section of oauth2 worflow by @momobel in https://github.com/requests/requests-oauthlib/pull/413
    • Add eBay compliance fix by @craiga in https://github.com/requests/requests-oauthlib/pull/456
    • Fix Docs generation - Improve Pipeline by @JonathanHuot in https://github.com/requests/requests-oauthlib/pull/459
    • Fix Sphinx error for oauth1 fetch_token documentation by @JonathanHuot in https://github.com/requests/requests-oauthlib/pull/462
    • Move from Travis to GitHub Actions by @JonathanHuot in https://github.com/requests/requests-oauthlib/pull/470
    • Add Spotify OAuth 2 Tutorial by @odysseusmax in https://github.com/requests/requests-oauthlib/pull/471
    • Update documentation for Python 3 by @gschizas in https://github.com/requests/requests-oauthlib/pull/435
    • docs: add the link to the Application Registration Portal by @Abdelkrim in https://github.com/requests/requests-oauthlib/pull/425
    • docs: rearrange and link spotify tutorial by @odysseusmax in https://github.com/requests/requests-oauthlib/pull/472
    • Update google.rst by @mrwangjianhui in https://github.com/requests/requests-oauthlib/pull/454
    • Add Python 3.8 & 3.9 as supported versions by @kaxil in https://github.com/requests/requests-oauthlib/pull/442
    • Update build badge for GitHub Actions by @hugovk in https://github.com/requests/requests-oauthlib/pull/475

    New Contributors

    • @danielfett made their first contribution in https://github.com/requests/requests-oauthlib/pull/389
    • @jtroussard made their first contribution in https://github.com/requests/requests-oauthlib/pull/397
    • @momobel made their first contribution in https://github.com/requests/requests-oauthlib/pull/413
    • @craiga made their first contribution in https://github.com/requests/requests-oauthlib/pull/456
    • @JonathanHuot made their first contribution in https://github.com/requests/requests-oauthlib/pull/459
    • @odysseusmax made their first contribution in https://github.com/requests/requests-oauthlib/pull/471
    • @gschizas made their first contribution in https://github.com/requests/requests-oauthlib/pull/435
    • @Abdelkrim made their first contribution in https://github.com/requests/requests-oauthlib/pull/425
    • @mrwangjianhui made their first contribution in https://github.com/requests/requests-oauthlib/pull/454
    • @kaxil made their first contribution in https://github.com/requests/requests-oauthlib/pull/442
    • @hugovk made their first contribution in https://github.com/requests/requests-oauthlib/pull/475

    Full Changelog: https://github.com/requests/requests-oauthlib/compare/v1.3.0...v1.3.1

    Source code(tar.gz)
    Source code(zip)
  • v1.3.0(Nov 2, 2021)

  • v1.2.0(Nov 2, 2021)

    • This project now depends on OAuthlib 3.0.0 and above. It does not support versions of OAuthlib before 3.0.0.
    • Updated oauth2 tests to use 'sess' for an OAuth2Session instance instead of auth because OAuth2Session objects and methods acceept an auth paramether which is typically an instance of requests.auth.HTTPBasicAuth
    • OAuth2Session.fetch_token previously tried to guess how and where to provide "client" and "user" credentials incorrectly. This was incompatible with some OAuth servers and incompatible with breaking changes in oauthlib that seek to correctly provide the client_id. The older implementation also did not raise the correct exceptions when username and password are not present on Legacy clients.
    • Avoid automatic netrc authentication for OAuth2Session.
    Source code(tar.gz)
    Source code(zip)
  • v1.1.0(Jan 9, 2019)

    • Adjusted version specifier for oauthlib dependency: this project is not yet compatible with oauthlib 3.0.0.
    • Dropped dependency on nose.
    • Minor changes to clean up the code and make it more readable/maintainable.
    Source code(tar.gz)
    Source code(zip)
  • v1.0.0(Jun 4, 2018)

    • Removed support for Python 2.6 and Python 3.3. This project now supports Python 2.7, and Python 3.4 and above.
    • Added several examples to the documentation.
    • Added plentymarkets compliance fix.
    • Added a token property to OAuth1Session, to match the corresponding token property on OAuth2Session.
    Source code(tar.gz)
    Source code(zip)
Django-react-firebase-auth - A web app showcasing OAuth2.0 + OpenID Connect using Firebase, Django-Rest-Framework and React

Demo app to show Django Rest Framework working with Firebase for authentication

Teshank Raut 6 Oct 13, 2022
Out-of-the-box support register, sign in, email verification and password recovery workflows for websites based on Django and MongoDB

Using djmongoauth What is it? djmongoauth provides out-of-the-box support for basic user management and additional operations including user registrat

hao 3 Oct 21, 2021
Foundation Auth Proxy is an abstraction on Foundations' authentication layer and is used to authenticate requests to Atlas's REST API.

foundations-auth-proxy Setup By default the server runs on http://0.0.0.0:5558. This can be changed via the arguments. Arguments: '-H' or '--host': ho

Dessa - Open Source 2 Jul 03, 2020
This script helps you log in to your LMS account and enter the currently running session

This script helps you log in to your LMS account and enter the currently running session, all in a second

Ali Ebrahimi 5 Sep 01, 2022
Basic auth for Django.

Basic auth for Django.

bichanna 2 Mar 25, 2022
Simple Login - Login Extension for Flask - maintainer @cuducos

Login Extension for Flask The simplest way to add login to flask! Top Contributors Add yourself, send a PR! How it works First install it from PyPI. p

Flask Extensions 181 Jan 01, 2023
This program automatically logs you into a Zoom session at your alloted time

This program automatically logs you into a Zoom session at your alloted time. Optionally you can choose to have end the session at your allotted time.

9 Sep 19, 2022
A full Rest-API With Oauth2 and JWT for request & response a JSON file Using FastAPI and SQLAlchemy 🔑

Pexon-Rest-API A full Rest-API for request & response a JSON file, Building a Simple WorkFlow that help you to Request a JSON File Format and Handling

Yasser Tahiri 15 Jul 22, 2022
A JSON Web Token authentication plugin for the Django REST Framework.

Simple JWT Abstract Simple JWT is a JSON Web Token authentication plugin for the Django REST Framework. For full documentation, visit django-rest-fram

Jazzband 3.2k Dec 29, 2022
Connect-4-AI - AI that plays Connect-4 using the minimax algorithm

Connect-4-AI Brief overview I coded up the Connect-4 (or four-in-a-row) game in

Favour Okeke 1 Feb 15, 2022
Multi-user accounts for Django projects

django-organizations Summary Groups and multi-user account management Author Ben Lopatin (http://benlopatin.com) Status Separate individual user ident

Ben Lopatin 1.1k Jan 02, 2023
This python package provides a simple password reset strategy for django rest framework

Django Rest Password Reset This python package provides a simple password reset strategy for django rest framework, where users can request password r

Anexia 363 Dec 24, 2022
Kube OpenID Connect is an application that can be used to easily enable authentication flows via OIDC for a kubernetes cluster

Kube OpenID Connect is an application that can be used to easily enable authentication flows via OIDC for a kubernetes cluster. Kubernetes supports OpenID Connect Tokens as a way to identify users wh

7 Nov 20, 2022
Complete Two-Factor Authentication for Django providing the easiest integration into most Django projects.

Django Two-Factor Authentication Complete Two-Factor Authentication for Django. Built on top of the one-time password framework django-otp and Django'

Bouke Haarsma 1.3k Jan 04, 2023
Django Admin Two-Factor Authentication, allows you to login django admin with google authenticator.

Django Admin Two-Factor Authentication Django Admin Two-Factor Authentication, allows you to login django admin with google authenticator. Why Django

Iman Karimi 9 Dec 07, 2022
:couple: Multi-user accounts for Django projects

django-organizations Summary Groups and multi-user account management Author Ben Lopatin (http://benlopatin.com) Status Separate individual user ident

Ben Lopatin 1.1k Jan 09, 2023
Graphical Password Authentication System.

Graphical Password Authentication System. This is used to increase the protection/security of a website. Our system is divided into further 4 layers of protection. Each layer is totally different and

Hassan Shahzad 12 Dec 16, 2022
OAuth2 goodies for the Djangonauts!

Django OAuth Toolkit OAuth2 goodies for the Djangonauts! If you are facing one or more of the following: Your Django app exposes a web API you want to

Jazzband 2.7k Jan 01, 2023
Cack facebook tidak login

Cack facebook tidak login

Angga Kurniawan 5 Dec 12, 2021
A JOSE implementation in Python

python-jose A JOSE implementation in Python Docs are available on ReadTheDocs. The JavaScript Object Signing and Encryption (JOSE) technologies - JSON

Michael Davis 1.2k Dec 28, 2022