Skip to main content

Python implementation of AT Protocol's XRPC + Lexicon

Project description

lexrpc Circle CI Coverage Status

Python implementation of AT Protocol's XRPC + Lexicon. lexrpc includes a simple XRPC client, server, and Flask web server integration. All three include full Lexicon support for validating inputs, outputs, and parameters against their schemas.

Install from PyPI with pip install lexrpc or pip install lexrpc[flask].

License: This project is placed in the public domain.

Client

The lexrpc client let you call methods dynamically by their NSIDs. To make a call, first instantiate a Client, then use NSIDs to make calls, passing input as a dict and parameters as kwargs. Here's an example of logging into the official Bluesky PDS and fetching the user's timeline:

from lexrpc import Client

client = Client()
session = client.com.atproto.server.createSession({
    'identifier': 'snarfed.bsky.social',
    'password': 'hunter2',
})
print('Logged in as', session['did'])

timeline = client.app.bsky.feed.getTimeline(limit=10)
print('First 10 posts:', json.dumps(timeline, indent=2))

By default, Client connects to the official bsky.social PDS and uses the official lexicons for app.bsky and com.atproto. You can connect to a different PDS or use custom lexicons by passing them to the Client constructor:

lexicons = [
  {
    "lexicon": 1,
    "id": "com.example.my-procedure",
    "defs": ...
  },
  ...
]
client = Client('my.server.com', lexicons=lexicons)
output = client.com.example.my_procedure({'foo': 'bar'}, baz=5)

Note that - characters in method NSIDs are converted to _s, eg the call above is for the method com.example.my-procedure.

Event stream methods with type subscription are generators that yield (header, payload) tuples sent by the server. They take parameters as kwargs, but no positional input.

for header, msg in client.com.example.count(start=1, end=10):
    print(header['t'])
    print(msg['num'])

Server

To implement an XRPC server, use the Server class. It validates parameters, inputs, and outputs. Use the method decorator to register method handlers and call to call them, whether from your web framework or anywhere else.

from lexrpc import Server

server = Server()

@server.method('com.example.my-query')
def my_query(input, num=None):
    output = {'foo': input['foo'], 'b': num + 1}
    return output

# Extract nsid and decode query parameters from an HTTP request,
# call the method, return the output in an HTTP response
nsid = request.path.removeprefix('/xrpc/')
input = request.json()
params = server.decode_params(nsid, request.query_params())
output = server.call(nsid, input, **params)
response.write_json(output)

You can also register a method handler with Server.register:

server.register('com.example.my-query', my_query_handler)

As with Client, you can use custom lexicons by passing them to the Server constructor:

lexicons = [
  {
    "lexicon": 1,
    "id": "com.example.myQuery",
    "defs": ...
  },
  ...
]
server = Server(lexicons=lexicons)

Event stream methods with type subscription should be generators that yield frames to send to the client. Each frame is a (header dict, payload dict) tuple that will be DAG-CBOR encoded and sent to the websocket client. Subscription methods take parameters as kwargs, but no positional input.

@server.method('com.example.count')
def count(start=None, end=None):
    for num in range(start, end):
        yield {'num': num}

Flask server

To serve XRPC methods in a Flask web app, first install the lexrpc package with the flask extra, eg pip install lexrpc[flask]. Then, instantiate a Server and register method handlers as described above. Finally, attach the server to your Flask app with flask_server.init_flask.

from flask import Flask
from lexrpc.flask_server import init_flask

# instantiate a Server like above
server = ...

app = Flask('my-server')
init_flask(server, app)

This configures the Flask app to serve the methods registered with the lexrpc server as per the spec. Each method is served at the path /xrpc/[NSID], procedures via POSTs and queries via GETs. Parameters are decoded from query parameters, input is taken from the JSON HTTP request body, and output is returned in the JSON HTTP response body. The Content-Type response header is set to application/json.

TODO

Release instructions

Here's how to package, test, and ship a new release.

  1. Run the unit tests.

    source local/bin/activate.csh
    python3 -m unittest discover
    
  2. Bump the version number in pyproject.toml and docs/conf.py. git grep the old version number to make sure it only appears in the changelog. Change the current changelog entry in README.md for this new version from unreleased to the current date.

  3. Build the docs. If you added any new modules, add them to the appropriate file(s) in docs/source/. Then run ./docs/build.sh. Check that the generated HTML looks fine by opening docs/_build/html/index.html and looking around.

  4. git commit -am 'release vX.Y'

  5. Upload to test.pypi.org for testing.

    python3 -m build
    setenv ver X.Y
    twine upload -r pypitest dist/lexrpc-$ver*
    
  6. Install from test.pypi.org.

    cd /tmp
    python3 -m venv local
    source local/bin/activate.csh
    pip3 uninstall lexrpc # make sure we force pip to use the uploaded version
    pip3 install --upgrade pip
    pip3 install -i https://test.pypi.org/simple --extra-index-url https://pypi.org/simple lexrpc==$ver
    deactivate
    
  7. Smoke test that the code trivially loads and runs.

    source local/bin/activate.csh
    python3
    # run test code below
    deactivate
    

    Test code to paste into the interpreter:

    from lexrpc import Server
    
    server = Server(lexicons=[{
        'lexicon': 1,
        'id': 'io.example.ping',
        'defs': {
            'main': {
                'type': 'query',
                'description': 'Ping the server',
                'parameters': {'message': { 'type': 'string' }},
                'output': {
                    'encoding': 'application/json',
                    'schema': {
                        'type': 'object',
                        'required': ['message'],
                        'properties': {'message': { 'type': 'string' }},
                    },
                },
            },
        },
    }])
    
    @server.method('io.example.ping')
    def ping(input, message=''):
        return {'message': message}
    
    print(server.call('io.example.ping', {}, message='hello world'))
    
  8. Tag the release in git. In the tag message editor, delete the generated comments at bottom, leave the first line blank (to omit the release "title" in github), put ### Notable changes on the second line, then copy and paste this version's changelog contents below it.

    git tag -a v$ver --cleanup=verbatim
    git push && git push --tags
    
  9. Click here to draft a new release on GitHub. Enter vX.Y in the Tag version box. Leave Release title empty. Copy ### Notable changes and the changelog contents into the description text box.

  10. Upload to pypi.org!

    twine upload dist/lexrpc-$ver.tar.gz dist/lexrpc-$ver-py3-none-any.whl
    
  11. Wait for the docs to build on Read the Docs, then check that they look ok.

  12. On the Versions page, check that the new version is active, If it's not, activate it in the Activate a Version section.

Changelog

0.4 - 2023-10-28

  • Bundle the official lexicons for app.bsky and com.atproto, use them by default.
  • Base:
    • Expose lexicons in defs attribute.
  • Client:
    • Add minimal auth support with access_token and refresh_token constructor kwargs and session attribute. If you use a Client to call com.atproto.server.createSession or com.atproto.server.refreshSession, the returned tokens will be automatically stored and used in future requests.
    • Bug fix: handle trailing slash on server address, eg http://ser.ver/ vs http://ser.ver.
    • Default server address to official https://bsky.social PDS.
    • Add default User-Agent: lexrpc (https://lexrpc.readthedocs.io/) request header.
  • server:
  • flask_server:
    • Return HTTP 405 error on HTTP requests to subscription (websocket) XRPCs.
    • Support the new Redirect exception.
    • Add the error field to the JSON response bodies for most error responses.

0.3 - 2023-08-29

  • Add array type support.
  • Add support for non-JSON input and output encodings.
  • Add subscription method type support over websockets.
  • Add headers kwarg to Client constructor.
  • Add new Server.register method for manually registering handlers.
  • Bug fix for server @method decorator.

0.2 - 2023-03-13

Bluesky's Lexicon design and schema handling is still actively changing, so this is an interim release. It generally supports the current lexicon design, but not full schema validation yet. I'm not yet trying to fast follow the changes too closely; as they settle down and stabilize, I'll put more effort into matching and fully implementing them. Stay tuned!

Breaking changes:

0.1 - 2022-12-13

Initial release!

Tested interoperability with the lexicon, xprc, and xrpc-server packages in bluesky-social/atproto. Lexicon and XRPC themselves are still very early and under active development; caveat hacker!

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lexrpc-0.4.tar.gz (39.1 kB view details)

Uploaded Source

Built Distribution

lexrpc-0.4-py3-none-any.whl (86.9 kB view details)

Uploaded Python 3

File details

Details for the file lexrpc-0.4.tar.gz.

File metadata

  • Download URL: lexrpc-0.4.tar.gz
  • Upload date:
  • Size: 39.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.1.1 pkginfo/1.8.1 requests/2.27.1 setuptools/68.2.2 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.17

File hashes

Hashes for lexrpc-0.4.tar.gz
Algorithm Hash digest
SHA256 3eba63e40005fadf37d81d3988c35f90b11d33062016b934f11ce0ffaa0a2242
MD5 f4ff7132f961ff46f99182da03ae5e4a
BLAKE2b-256 a3f07177a46b52dca46732f53dcfda88086a6ba287197b20ded53f177072f658

See more details on using hashes here.

File details

Details for the file lexrpc-0.4-py3-none-any.whl.

File metadata

  • Download URL: lexrpc-0.4-py3-none-any.whl
  • Upload date:
  • Size: 86.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.1.1 pkginfo/1.8.1 requests/2.27.1 setuptools/68.2.2 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.17

File hashes

Hashes for lexrpc-0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 34dfb75101be59d3e56655fcf0008dac9585b1b43dee0baba1876c54ab26ee99
MD5 5c4607ec3bb21c931a87ebef10980fa7
BLAKE2b-256 698fa17e6dab43f0c8605e5c2dc1716413db535ecd947068f1af174bb0499a82

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page