arroba


Namearroba JSON
Version 0.5 PyPI version JSON
download
home_page
SummaryPython implementation of Bluesky PDS and AT Protocol, including repo, MST, and sync methods
upload_time2024-03-16 20:33:00
maintainer
docs_urlNone
author
requires_python>=3.9
license
keywords arroba at protocol atp bluesky
VCS
bugtrack_url
requirements dag_json carbox lexrpc async-timeout attrs bases blinker cachetools cbor2 certifi cffi charset-normalizer click cryptography dag-cbor dnspython eventlet Flask flask-sock google-api-core google-auth google-cloud-appengine-logging google-cloud-audit-log google-cloud-core google-cloud-datastore google-cloud-error-reporting google-cloud-logging google-cloud-ndb googleapis-common-protos greenlet grpcio grpcio-status gunicorn h11 idna importlib-metadata itsdangerous Jinja2 jsonschema jsonschema-specifications MarkupSafe multiformats multiformats-config packaging proto-plus protobuf pyasn1 pyasn1-modules pycparser pycryptodome pyjwt pymemcache pytz redis referencing requests rpds-py rsa simple-websocket six typing-extensions typing-validation urllib3 Werkzeug wsproto zipp
Travis-CI No Travis.
coveralls test coverage No coveralls.
            arroba [![Circle CI](https://circleci.com/gh/snarfed/arroba.svg?style=svg)](https://circleci.com/gh/snarfed/arroba) [![Coverage Status](https://coveralls.io/repos/github/snarfed/arroba/badge.svg?branch=main)](https://coveralls.io/github/snarfed/arroba?branch=master)
===

Python implementation of [Bluesky](https://blueskyweb.xyz/) [PDS](https://atproto.com/guides/data-repos) and [AT Protocol](https://atproto.com/specs/atp), including data repository, Merkle search tree, and [com.atproto.sync XRPC methods](https://atproto.com/lexicons/com-atproto-sync).

You can build your own PDS on top of arroba with just a few lines of Python and run it in any WSGI server. You can build a more involved PDS with custom logic and behavior. Or you can build a different ATProto service, eg an [AppView, relay (née BGS)](https://blueskyweb.xyz/blog/5-5-2023-federation-architecture), or something entirely new!

Install [from PyPI](https://pypi.org/project/arroba/) with `pip install arroba`.

_Arroba_ is the Spanish word for the [@ character](https://en.wikipedia.org/wiki/At_sign) ("at sign").

License: This project is placed in the public domain. You may also use it under the [CC0 License](https://creativecommons.org/publicdomain/zero/1.0/).

* [Usage](#usage)
* [Overview](#overview)
* [Configuration](#configuration)
* [Docs](https://arroba.readthedocs.io/)
* [Changelog](#changelog)
* [Release instructions](#release-instructions)


## Usage

Here's minimal example code for a multi-repo PDS on top of arroba and [Flask](https://flask.palletsprojects.com/):

```py
from flask import Flask
from google.cloud import ndb
from lexrpc.flask_server import init_flask

from arroba import server
from arroba.datastore_storage import DatastoreStorage
from arroba.xrpc_sync import send_new_commits

server.storage = DatastoreStorage()
server.repo.callback = lambda _: send_new_commits()  # to subscribeRepos

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

# for Google Cloud Datastore
ndb_client = ndb.Client()

def ndb_context_middleware(wsgi_app):
    def wrapper(environ, start_response):
        with ndb_client.context():
            return wsgi_app(environ, start_response)
    return wrapper

app.wsgi_app = ndb_context_middleware(app.wsgi_app)
```

See [`app.py`](https://github.com/snarfed/arroba/blob/main/app.py) for a more comprehensive example, including a CORS handler for `OPTIONS` preflight requests and a catch-all `app.bsky.*` XRPC handler that proxies requests to the AppView.


## Overview

Arroba consists of these parts:

* **Data structures**:
  * [`Repo`](https://arroba.readthedocs.io/en/stable/source/arroba.html#arroba.repo.Repo)
  * [`MST`](https://arroba.readthedocs.io/en/stable/source/arroba.html#arroba.mst.MST) (Merkle search tree)
* **Storage**:
  * [`Storage`](https://arroba.readthedocs.io/en/stable/source/arroba.html#arroba.storage.Storage) abstract base class
  * [`DatastoreStorage`](https://arroba.readthedocs.io/en/stable/source/arroba.html#arroba.datastore_storage.DatastoreStorage) (uses [Google Cloud Datastore](https://cloud.google.com/datastore/docs/))
  * [TODO: filesystem storage](https://github.com/snarfed/arroba/issues/5)
* **XRPC handlers**:
  * [`com.atproto.repo`](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.xrpc_repo)
  * [`com.atproto.server`](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.xrpc_server)
  * [`com.atproto.sync`](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.xrpc_sync)
* **Utilities**:
  * [`did`](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.did): create and resolve [`did:plc`](https://atproto.com/specs/did-plc)s, [`did:web`](https://w3c-ccg.github.io/did-method-web/)s, and [domain handles](https://atproto.com/specs/handle)
  * [`diff`](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.diff): find the deterministic minimal difference between two [`MST`](https://arroba.readthedocs.io/en/stable/source/arroba.html#arroba.mst.MST)s
  * [`util`](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.util): miscellaneous utilities for [TIDs](https://atproto.com/specs/record-key#record-key-type-tid), [AT URIs](https://atproto.com/specs/at-uri-scheme), [signing and verifying signatures](https://atproto.com/specs/repository#commit-objects), [generating JWTs](https://atproto.com/specs/xrpc#inter-service-authentication-temporary-specification), encoding/decoding, and more


## Configuration

Configure arroba with these environment variables:

* `APPVIEW_HOST`, default `api.bsky-sandbox.dev`
* `RELAY_HOST`, default `bgs.bsky-sandbox.dev`
* `PLC_HOST`, default `plc.bsky-sandbox.dev`
* `PDS_HOST`, where you're running your PDS

Optional, only used in [com.atproto.repo](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.xrpc_repo) and [com.atproto.server](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.xrpc_server) XRPC handlers:

* `REPO_TOKEN`, static token to use as both `accessJwt` and `refreshJwt`, defaults to contents of `repo_token` file. Not required to be an actual JWT. If not set, XRPC methods that require auth will return HTTP 501 Not Implemented.

<!-- Only used in app.py:
* `REPO_DID`, repo user's DID, defaults to contents of `repo_did` file
* `REPO_HANDLE`, repo user's domain handle, defaults to `did:plc:*.json` file
* `REPO_PASSWORD`, repo user's password, defaults to contents of `repo_password` file
* `REPO_PRIVKEY`, repo user's private key in PEM format, defaults to contents of `privkey.pem` file
-->


## Changelog

### 0.5 - 2024-03-16

* Bug fix: base32-encode TIDs in record keys, `at://` URIs, commit `rev`s, etc. Before, we were using the integer UNIX timestamp directly, which happened to be the same 13 character length. Oops.
* Switch from `BGS_HOST` environment variable to `RELAY_HOST`. `BGS_HOST` is still supported for backward compatibility.
* `datastore_storage`:
  * Bug fix for `DatastoreStorage.last_seq`, handle new NSID.
  * Add new `AtpRemoteBlob` class for storing "remote" blobs, available at public HTTP URLs, that we don't store ourselves.
* `did`:
  * `create_plc`: strip padding from genesis operation signature (for [did-method-plc#54](https://github.com/did-method-plc/did-method-plc/pull/54), [atproto#1839](https://github.com/bluesky-social/atproto/pull/1839)).
  * `resolve_handle`: return None on bad domain, eg `.foo.com`.
  * `resolve_handle` bug fix: handle `charset` specifier in HTTPS method response `Content-Type`.
* `util`:
  * `new_key`: add `seed` kwarg to allow deterministic key generation.
* `xrpc_repo`:
  * `getRecord`: try to load record locally first; if not available, forward to AppView.
* `xrpc_sync`:
  * Implement `getBlob`, right now only based on "remote" blobs stored in `AtpRemoteBlob`s in datastore storage.

### 0.4 - 2023-09-19

* Migrate to [ATProto repo v3](https://atproto.com/blog/repo-sync-update). Specifically, the existing `subscribeRepos` sequence number is reused as the new `rev` field in commits. ([Discussion.](https://github.com/bluesky-social/atproto/discussions/1607)).
* Add new `did` module with utilities to create and resolve `did:plc`s and resolve `did:web`s.
* Add new `util.service_jwt` function that generates [ATProto inter-service JWTs](https://atproto.com/specs/xrpc#inter-service-authentication-temporary-specification).
* `Repo`:
  * Add new `signing_key`/`rotation_key` attributes. Generate store, and load both in `datastore_storage`.
  * Remove `format_init_commit`, migrate existing calls to `format_commit`.
* `Storage`:
  * Rename `read_from_seq` => `read_blocks_by_seq` (and in `MemoryStorage` and `DatastoreStorage`), add new `read_commits_by_seq` method.
  * Merge `load_repo` `did`/`handle` kwargs into `did_or_handle`.
* XRPCs:
  * Make `subscribeRepos` check storage for all new commits every time it wakes up.
    * As part of this, replace `xrpc_sync.enqueue_commit` with new `send_new_commits` function that takes no parameters.
  * Drop bundled `app.bsky`/`com.atproto` lexicons, use [lexrpc](https://lexrpc.readthedocs.io/)'s instead.

### 0.3 - 2023-08-29

Big milestone: arroba is successfully federating with the [ATProto sandbox](https://atproto.com/blog/federation-developer-sandbox)! See [app.py](https://github.com/snarfed/arroba/blob/main/app.py) for the minimal demo code needed to wrap arroba in a fully functional PDS.

* Add Google Cloud Datastore implementation of repo storage.
* Implement `com.atproto` XRPC methods needed to federate with sandbox, including most of `repo` and `sync`.
  * Notably, includes `subscribeRepos` server side over websocket.
* ...and much more.

### 0.2 - 2023-05-18

Implement repo and commit chain in new Repo class, including pluggable storage. This completes the first pass at all PDS data structures. Next release will include initial implementations of the `com.atproto.sync.*` XRPC methods.

### 0.1 - 2023-04-30

Initial release! Still very in progress. MST, Walker, and Diff classes are mostly complete and working. Repo, commits, and sync XRPC methods are still in progress.


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

1. Run the unit tests.

    ```sh
    source local/bin/activate.csh
    python3 -m unittest discover
    ```
1. 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.
1. 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.
1. ```sh
   setenv ver X.Y
   git commit -am "release v$ver"
   ```
1. Upload to [test.pypi.org](https://test.pypi.org/) for testing.

    ```sh
    python3 -m build
    twine upload -r pypitest dist/arroba-$ver*
    ```
1. Install from test.pypi.org.

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

    ```sh
    source local/bin/activate.csh
    python3
    # TODO: test code
    deactivate
    ```
1. 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.

    ```sh
    git tag -a v$ver --cleanup=verbatim
    git push && git push --tags
    ```
1. [Click here to draft a new release on GitHub.](https://github.com/snarfed/arroba/releases/new) Enter `vX.Y` in the _Tag version_ box. Leave _Release title_ empty. Copy `### Notable changes` and the changelog contents into the description text box.
1. Upload to [pypi.org](https://pypi.org/)!

    ```sh
    twine upload dist/arroba-$ver*
    ```
1. [Wait for the docs to build on Read the Docs](https://readthedocs.org/projects/arroba/builds/), then check that they look ok.
1. On the [Versions page](https://readthedocs.org/projects/arroba/versions/), check that the new version is active, If it's not, activate it in the _Activate a Version_ section.

            

Raw data

            {
    "_id": null,
    "home_page": "",
    "name": "arroba",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.9",
    "maintainer_email": "",
    "keywords": "arroba,AT Protocol,ATP,Bluesky",
    "author": "",
    "author_email": "Ryan Barrett <arroba@ryanb.org>",
    "download_url": "https://files.pythonhosted.org/packages/5e/0f/dd5087f98d736906c4498d93c5c2ce84a9af1d7788e2a422b2ad97a134eb/arroba-0.5.tar.gz",
    "platform": null,
    "description": "arroba [![Circle CI](https://circleci.com/gh/snarfed/arroba.svg?style=svg)](https://circleci.com/gh/snarfed/arroba) [![Coverage Status](https://coveralls.io/repos/github/snarfed/arroba/badge.svg?branch=main)](https://coveralls.io/github/snarfed/arroba?branch=master)\n===\n\nPython implementation of [Bluesky](https://blueskyweb.xyz/) [PDS](https://atproto.com/guides/data-repos) and [AT Protocol](https://atproto.com/specs/atp), including data repository, Merkle search tree, and [com.atproto.sync XRPC methods](https://atproto.com/lexicons/com-atproto-sync).\n\nYou can build your own PDS on top of arroba with just a few lines of Python and run it in any WSGI server. You can build a more involved PDS with custom logic and behavior. Or you can build a different ATProto service, eg an [AppView, relay (n\u00e9e BGS)](https://blueskyweb.xyz/blog/5-5-2023-federation-architecture), or something entirely new!\n\nInstall [from PyPI](https://pypi.org/project/arroba/) with `pip install arroba`.\n\n_Arroba_ is the Spanish word for the [@ character](https://en.wikipedia.org/wiki/At_sign) (\"at sign\").\n\nLicense: This project is placed in the public domain. You may also use it under the [CC0 License](https://creativecommons.org/publicdomain/zero/1.0/).\n\n* [Usage](#usage)\n* [Overview](#overview)\n* [Configuration](#configuration)\n* [Docs](https://arroba.readthedocs.io/)\n* [Changelog](#changelog)\n* [Release instructions](#release-instructions)\n\n\n## Usage\n\nHere's minimal example code for a multi-repo PDS on top of arroba and [Flask](https://flask.palletsprojects.com/):\n\n```py\nfrom flask import Flask\nfrom google.cloud import ndb\nfrom lexrpc.flask_server import init_flask\n\nfrom arroba import server\nfrom arroba.datastore_storage import DatastoreStorage\nfrom arroba.xrpc_sync import send_new_commits\n\nserver.storage = DatastoreStorage()\nserver.repo.callback = lambda _: send_new_commits()  # to subscribeRepos\n\napp = Flask('my-pds')\ninit_flask(server.server, app)\n\n# for Google Cloud Datastore\nndb_client = ndb.Client()\n\ndef ndb_context_middleware(wsgi_app):\n    def wrapper(environ, start_response):\n        with ndb_client.context():\n            return wsgi_app(environ, start_response)\n    return wrapper\n\napp.wsgi_app = ndb_context_middleware(app.wsgi_app)\n```\n\nSee [`app.py`](https://github.com/snarfed/arroba/blob/main/app.py) for a more comprehensive example, including a CORS handler for `OPTIONS` preflight requests and a catch-all `app.bsky.*` XRPC handler that proxies requests to the AppView.\n\n\n## Overview\n\nArroba consists of these parts:\n\n* **Data structures**:\n  * [`Repo`](https://arroba.readthedocs.io/en/stable/source/arroba.html#arroba.repo.Repo)\n  * [`MST`](https://arroba.readthedocs.io/en/stable/source/arroba.html#arroba.mst.MST) (Merkle search tree)\n* **Storage**:\n  * [`Storage`](https://arroba.readthedocs.io/en/stable/source/arroba.html#arroba.storage.Storage) abstract base class\n  * [`DatastoreStorage`](https://arroba.readthedocs.io/en/stable/source/arroba.html#arroba.datastore_storage.DatastoreStorage) (uses [Google Cloud Datastore](https://cloud.google.com/datastore/docs/))\n  * [TODO: filesystem storage](https://github.com/snarfed/arroba/issues/5)\n* **XRPC handlers**:\n  * [`com.atproto.repo`](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.xrpc_repo)\n  * [`com.atproto.server`](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.xrpc_server)\n  * [`com.atproto.sync`](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.xrpc_sync)\n* **Utilities**:\n  * [`did`](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.did): create and resolve [`did:plc`](https://atproto.com/specs/did-plc)s, [`did:web`](https://w3c-ccg.github.io/did-method-web/)s, and [domain handles](https://atproto.com/specs/handle)\n  * [`diff`](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.diff): find the deterministic minimal difference between two [`MST`](https://arroba.readthedocs.io/en/stable/source/arroba.html#arroba.mst.MST)s\n  * [`util`](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.util): miscellaneous utilities for [TIDs](https://atproto.com/specs/record-key#record-key-type-tid), [AT URIs](https://atproto.com/specs/at-uri-scheme), [signing and verifying signatures](https://atproto.com/specs/repository#commit-objects), [generating JWTs](https://atproto.com/specs/xrpc#inter-service-authentication-temporary-specification), encoding/decoding, and more\n\n\n## Configuration\n\nConfigure arroba with these environment variables:\n\n* `APPVIEW_HOST`, default `api.bsky-sandbox.dev`\n* `RELAY_HOST`, default `bgs.bsky-sandbox.dev`\n* `PLC_HOST`, default `plc.bsky-sandbox.dev`\n* `PDS_HOST`, where you're running your PDS\n\nOptional, only used in [com.atproto.repo](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.xrpc_repo) and [com.atproto.server](https://arroba.readthedocs.io/en/stable/source/arroba.html#module-arroba.xrpc_server) XRPC handlers:\n\n* `REPO_TOKEN`, static token to use as both `accessJwt` and `refreshJwt`, defaults to contents of `repo_token` file. Not required to be an actual JWT. If not set, XRPC methods that require auth will return HTTP 501 Not Implemented.\n\n<!-- Only used in app.py:\n* `REPO_DID`, repo user's DID, defaults to contents of `repo_did` file\n* `REPO_HANDLE`, repo user's domain handle, defaults to `did:plc:*.json` file\n* `REPO_PASSWORD`, repo user's password, defaults to contents of `repo_password` file\n* `REPO_PRIVKEY`, repo user's private key in PEM format, defaults to contents of `privkey.pem` file\n-->\n\n\n## Changelog\n\n### 0.5 - 2024-03-16\n\n* Bug fix: base32-encode TIDs in record keys, `at://` URIs, commit `rev`s, etc. Before, we were using the integer UNIX timestamp directly, which happened to be the same 13 character length. Oops.\n* Switch from `BGS_HOST` environment variable to `RELAY_HOST`. `BGS_HOST` is still supported for backward compatibility.\n* `datastore_storage`:\n  * Bug fix for `DatastoreStorage.last_seq`, handle new NSID.\n  * Add new `AtpRemoteBlob` class for storing \"remote\" blobs, available at public HTTP URLs, that we don't store ourselves.\n* `did`:\n  * `create_plc`: strip padding from genesis operation signature (for [did-method-plc#54](https://github.com/did-method-plc/did-method-plc/pull/54), [atproto#1839](https://github.com/bluesky-social/atproto/pull/1839)).\n  * `resolve_handle`: return None on bad domain, eg `.foo.com`.\n  * `resolve_handle` bug fix: handle `charset` specifier in HTTPS method response `Content-Type`.\n* `util`:\n  * `new_key`: add `seed` kwarg to allow deterministic key generation.\n* `xrpc_repo`:\n  * `getRecord`: try to load record locally first; if not available, forward to AppView.\n* `xrpc_sync`:\n  * Implement `getBlob`, right now only based on \"remote\" blobs stored in `AtpRemoteBlob`s in datastore storage.\n\n### 0.4 - 2023-09-19\n\n* Migrate to [ATProto repo v3](https://atproto.com/blog/repo-sync-update). Specifically, the existing `subscribeRepos` sequence number is reused as the new `rev` field in commits. ([Discussion.](https://github.com/bluesky-social/atproto/discussions/1607)).\n* Add new `did` module with utilities to create and resolve `did:plc`s and resolve `did:web`s.\n* Add new `util.service_jwt` function that generates [ATProto inter-service JWTs](https://atproto.com/specs/xrpc#inter-service-authentication-temporary-specification).\n* `Repo`:\n  * Add new `signing_key`/`rotation_key` attributes. Generate store, and load both in `datastore_storage`.\n  * Remove `format_init_commit`, migrate existing calls to `format_commit`.\n* `Storage`:\n  * Rename `read_from_seq` => `read_blocks_by_seq` (and in `MemoryStorage` and `DatastoreStorage`), add new `read_commits_by_seq` method.\n  * Merge `load_repo` `did`/`handle` kwargs into `did_or_handle`.\n* XRPCs:\n  * Make `subscribeRepos` check storage for all new commits every time it wakes up.\n    * As part of this, replace `xrpc_sync.enqueue_commit` with new `send_new_commits` function that takes no parameters.\n  * Drop bundled `app.bsky`/`com.atproto` lexicons, use [lexrpc](https://lexrpc.readthedocs.io/)'s instead.\n\n### 0.3 - 2023-08-29\n\nBig milestone: arroba is successfully federating with the [ATProto sandbox](https://atproto.com/blog/federation-developer-sandbox)! See [app.py](https://github.com/snarfed/arroba/blob/main/app.py) for the minimal demo code needed to wrap arroba in a fully functional PDS.\n\n* Add Google Cloud Datastore implementation of repo storage.\n* Implement `com.atproto` XRPC methods needed to federate with sandbox, including most of `repo` and `sync`.\n  * Notably, includes `subscribeRepos` server side over websocket.\n* ...and much more.\n\n### 0.2 - 2023-05-18\n\nImplement repo and commit chain in new Repo class, including pluggable storage. This completes the first pass at all PDS data structures. Next release will include initial implementations of the `com.atproto.sync.*` XRPC methods.\n\n### 0.1 - 2023-04-30\n\nInitial release! Still very in progress. MST, Walker, and Diff classes are mostly complete and working. Repo, commits, and sync XRPC methods are still in progress.\n\n\nRelease instructions\n---\nHere's how to package, test, and ship a new release.\n\n1. Run the unit tests.\n\n    ```sh\n    source local/bin/activate.csh\n    python3 -m unittest discover\n    ```\n1. 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.\n1. 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.\n1. ```sh\n   setenv ver X.Y\n   git commit -am \"release v$ver\"\n   ```\n1. Upload to [test.pypi.org](https://test.pypi.org/) for testing.\n\n    ```sh\n    python3 -m build\n    twine upload -r pypitest dist/arroba-$ver*\n    ```\n1. Install from test.pypi.org.\n\n    ```sh\n    cd /tmp\n    python3 -m venv local\n    source local/bin/activate.csh\n    # make sure we force pip to use the uploaded version\n    pip3 uninstall arroba\n    pip3 install --upgrade pip\n    pip3 install -i https://test.pypi.org/simple --extra-index-url https://pypi.org/simple arroba==$ver\n    deactivate\n    ```\n1. Smoke test that the code trivially loads and runs.\n\n    ```sh\n    source local/bin/activate.csh\n    python3\n    # TODO: test code\n    deactivate\n    ```\n1. 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.\n\n    ```sh\n    git tag -a v$ver --cleanup=verbatim\n    git push && git push --tags\n    ```\n1. [Click here to draft a new release on GitHub.](https://github.com/snarfed/arroba/releases/new) Enter `vX.Y` in the _Tag version_ box. Leave _Release title_ empty. Copy `### Notable changes` and the changelog contents into the description text box.\n1. Upload to [pypi.org](https://pypi.org/)!\n\n    ```sh\n    twine upload dist/arroba-$ver*\n    ```\n1. [Wait for the docs to build on Read the Docs](https://readthedocs.org/projects/arroba/builds/), then check that they look ok.\n1. On the [Versions page](https://readthedocs.org/projects/arroba/versions/), check that the new version is active, If it's not, activate it in the _Activate a Version_ section.\n",
    "bugtrack_url": null,
    "license": "",
    "summary": "Python implementation of Bluesky PDS and AT Protocol, including repo, MST, and sync methods",
    "version": "0.5",
    "project_urls": {
        "Documentation": "https://arroba.readthedocs.io/",
        "Homepage": "https://github.com/snarfed/arroba"
    },
    "split_keywords": [
        "arroba",
        "at protocol",
        "atp",
        "bluesky"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "e100e50515c81c3896f817190e2712cf03c5777340f378e4f308ed4e07628018",
                "md5": "ffc9b4b9d44b14ae987cdbc366d6fd09",
                "sha256": "a920b3094c28dd140730a240bf822309a30c94c40876caf30c0d04cdea21439a"
            },
            "downloads": -1,
            "filename": "arroba-0.5-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "ffc9b4b9d44b14ae987cdbc366d6fd09",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.9",
            "size": 42985,
            "upload_time": "2024-03-16T20:32:58",
            "upload_time_iso_8601": "2024-03-16T20:32:58.620286Z",
            "url": "https://files.pythonhosted.org/packages/e1/00/e50515c81c3896f817190e2712cf03c5777340f378e4f308ed4e07628018/arroba-0.5-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "5e0fdd5087f98d736906c4498d93c5c2ce84a9af1d7788e2a422b2ad97a134eb",
                "md5": "b2ed9cd4fdfd8b3254a77768fdb89ac7",
                "sha256": "6b8cb8e9032588f23b2006157d7ad739e75202b8f2255a8cdae5043a93476dae"
            },
            "downloads": -1,
            "filename": "arroba-0.5.tar.gz",
            "has_sig": false,
            "md5_digest": "b2ed9cd4fdfd8b3254a77768fdb89ac7",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.9",
            "size": 42667,
            "upload_time": "2024-03-16T20:33:00",
            "upload_time_iso_8601": "2024-03-16T20:33:00.690836Z",
            "url": "https://files.pythonhosted.org/packages/5e/0f/dd5087f98d736906c4498d93c5c2ce84a9af1d7788e2a422b2ad97a134eb/arroba-0.5.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-03-16 20:33:00",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "snarfed",
    "github_project": "arroba",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "circle": true,
    "requirements": [
        {
            "name": "dag_json",
            "specs": []
        },
        {
            "name": "carbox",
            "specs": []
        },
        {
            "name": "lexrpc",
            "specs": []
        },
        {
            "name": "async-timeout",
            "specs": [
                [
                    "==",
                    "4.0.2"
                ]
            ]
        },
        {
            "name": "attrs",
            "specs": [
                [
                    "==",
                    "23.2.0"
                ]
            ]
        },
        {
            "name": "bases",
            "specs": [
                [
                    "==",
                    "0.3.0"
                ]
            ]
        },
        {
            "name": "blinker",
            "specs": [
                [
                    "==",
                    "1.6.2"
                ]
            ]
        },
        {
            "name": "cachetools",
            "specs": [
                [
                    "==",
                    "5.3.1"
                ]
            ]
        },
        {
            "name": "cbor2",
            "specs": [
                [
                    "==",
                    "5.6.2"
                ]
            ]
        },
        {
            "name": "certifi",
            "specs": [
                [
                    "==",
                    "2024.2.2"
                ]
            ]
        },
        {
            "name": "cffi",
            "specs": [
                [
                    "==",
                    "1.16.0"
                ]
            ]
        },
        {
            "name": "charset-normalizer",
            "specs": [
                [
                    "==",
                    "3.3.2"
                ]
            ]
        },
        {
            "name": "click",
            "specs": [
                [
                    "==",
                    "8.1.7"
                ]
            ]
        },
        {
            "name": "cryptography",
            "specs": [
                [
                    "==",
                    "42.0.5"
                ]
            ]
        },
        {
            "name": "dag-cbor",
            "specs": [
                [
                    "==",
                    "0.3.3"
                ]
            ]
        },
        {
            "name": "dnspython",
            "specs": [
                [
                    "==",
                    "2.6.1"
                ]
            ]
        },
        {
            "name": "eventlet",
            "specs": [
                [
                    "==",
                    "0.33.3"
                ]
            ]
        },
        {
            "name": "Flask",
            "specs": [
                [
                    "==",
                    "3.0.2"
                ]
            ]
        },
        {
            "name": "flask-sock",
            "specs": [
                [
                    "==",
                    "0.7.0"
                ]
            ]
        },
        {
            "name": "google-api-core",
            "specs": [
                [
                    "==",
                    "2.11.1"
                ]
            ]
        },
        {
            "name": "google-auth",
            "specs": [
                [
                    "==",
                    "2.15.0"
                ]
            ]
        },
        {
            "name": "google-cloud-appengine-logging",
            "specs": [
                [
                    "==",
                    "1.4.3"
                ]
            ]
        },
        {
            "name": "google-cloud-audit-log",
            "specs": [
                [
                    "==",
                    "0.2.5"
                ]
            ]
        },
        {
            "name": "google-cloud-core",
            "specs": [
                [
                    "==",
                    "2.3.2"
                ]
            ]
        },
        {
            "name": "google-cloud-datastore",
            "specs": [
                [
                    "==",
                    "2.16.1"
                ]
            ]
        },
        {
            "name": "google-cloud-error-reporting",
            "specs": [
                [
                    "==",
                    "1.10.0"
                ]
            ]
        },
        {
            "name": "google-cloud-logging",
            "specs": [
                [
                    "==",
                    "3.9.0"
                ]
            ]
        },
        {
            "name": "google-cloud-ndb",
            "specs": [
                [
                    "==",
                    "2.3.0"
                ]
            ]
        },
        {
            "name": "googleapis-common-protos",
            "specs": [
                [
                    "==",
                    "1.62.0"
                ]
            ]
        },
        {
            "name": "greenlet",
            "specs": [
                [
                    "==",
                    "2.0.2"
                ]
            ]
        },
        {
            "name": "grpcio",
            "specs": [
                [
                    "==",
                    "1.62.0"
                ]
            ]
        },
        {
            "name": "grpcio-status",
            "specs": [
                [
                    "==",
                    "1.62.0"
                ]
            ]
        },
        {
            "name": "gunicorn",
            "specs": [
                [
                    "==",
                    "21.2.0"
                ]
            ]
        },
        {
            "name": "h11",
            "specs": [
                [
                    "==",
                    "0.14.0"
                ]
            ]
        },
        {
            "name": "idna",
            "specs": [
                [
                    "==",
                    "3.6"
                ]
            ]
        },
        {
            "name": "importlib-metadata",
            "specs": [
                [
                    "==",
                    "6.8.0"
                ]
            ]
        },
        {
            "name": "itsdangerous",
            "specs": [
                [
                    "==",
                    "2.1.2"
                ]
            ]
        },
        {
            "name": "Jinja2",
            "specs": [
                [
                    "==",
                    "3.1.3"
                ]
            ]
        },
        {
            "name": "jsonschema",
            "specs": [
                [
                    "==",
                    "4.18.6"
                ]
            ]
        },
        {
            "name": "jsonschema-specifications",
            "specs": [
                [
                    "==",
                    "2023.7.1"
                ]
            ]
        },
        {
            "name": "MarkupSafe",
            "specs": [
                [
                    "==",
                    "2.1.3"
                ]
            ]
        },
        {
            "name": "multiformats",
            "specs": [
                [
                    "==",
                    "0.3.1.post4"
                ]
            ]
        },
        {
            "name": "multiformats-config",
            "specs": [
                [
                    "==",
                    "0.3.1"
                ]
            ]
        },
        {
            "name": "packaging",
            "specs": [
                [
                    "==",
                    "23.1"
                ]
            ]
        },
        {
            "name": "proto-plus",
            "specs": [
                [
                    "==",
                    "1.23.0"
                ]
            ]
        },
        {
            "name": "protobuf",
            "specs": [
                [
                    "==",
                    "4.24.3"
                ]
            ]
        },
        {
            "name": "pyasn1",
            "specs": [
                [
                    "==",
                    "0.5.1"
                ]
            ]
        },
        {
            "name": "pyasn1-modules",
            "specs": [
                [
                    "==",
                    "0.3.0"
                ]
            ]
        },
        {
            "name": "pycparser",
            "specs": [
                [
                    "==",
                    "2.21"
                ]
            ]
        },
        {
            "name": "pycryptodome",
            "specs": [
                [
                    "==",
                    "3.20.0"
                ]
            ]
        },
        {
            "name": "pyjwt",
            "specs": [
                [
                    "==",
                    "2.8.0"
                ]
            ]
        },
        {
            "name": "pymemcache",
            "specs": [
                [
                    "==",
                    "4.0.0"
                ]
            ]
        },
        {
            "name": "pytz",
            "specs": [
                [
                    "==",
                    "2023.3"
                ]
            ]
        },
        {
            "name": "redis",
            "specs": [
                [
                    "==",
                    "4.6.0"
                ]
            ]
        },
        {
            "name": "referencing",
            "specs": [
                [
                    "==",
                    "0.30.0"
                ]
            ]
        },
        {
            "name": "requests",
            "specs": [
                [
                    "==",
                    "2.31.0"
                ]
            ]
        },
        {
            "name": "rpds-py",
            "specs": [
                [
                    "==",
                    "0.9.2"
                ]
            ]
        },
        {
            "name": "rsa",
            "specs": [
                [
                    "==",
                    "4.9"
                ]
            ]
        },
        {
            "name": "simple-websocket",
            "specs": [
                [
                    "==",
                    "1.0.0"
                ]
            ]
        },
        {
            "name": "six",
            "specs": [
                [
                    "==",
                    "1.16.0"
                ]
            ]
        },
        {
            "name": "typing-extensions",
            "specs": [
                [
                    "==",
                    "4.9.0"
                ]
            ]
        },
        {
            "name": "typing-validation",
            "specs": [
                [
                    "==",
                    "1.2.10.post4"
                ]
            ]
        },
        {
            "name": "urllib3",
            "specs": [
                [
                    "==",
                    "2.2.1"
                ]
            ]
        },
        {
            "name": "Werkzeug",
            "specs": [
                [
                    "==",
                    "3.0.1"
                ]
            ]
        },
        {
            "name": "wsproto",
            "specs": [
                [
                    "==",
                    "1.2.0"
                ]
            ]
        },
        {
            "name": "zipp",
            "specs": [
                [
                    "==",
                    "3.16.2"
                ]
            ]
        }
    ],
    "lcname": "arroba"
}
        
Elapsed time: 0.21552s