💾 Archived View for tris.fyi › projects › amethyst › extension.gmi captured on 2023-09-28 at 15:42:06. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2022-01-08)

-=-=-=-=-=-=-

Extending Amethyst

You can extend Amethyst's functionality by writing your own "resource provider" modules and including them in a Python package. This page describes the "resource provider" interface you must implement and how provider discovery works.

A simple example

Resource providers are simply coroutines that take a "request context" as a parameter and return a response. Here is an example of a very simple resource provider:

from amethyst.response import Status, Response
async def hello_world_provider(ctx):
    return Response(Status.SUCCESS, "text/plain", b"Hello, world!")

Usually, providers have some kind of parameters that must be provided to function. For example, the filesystem provider takes a root directory. Let's extend this example to a generic greeting provider:

from amethyst.response import Status, Response

def greeting_provider_factory(what="world"):
    async def greeting_provider(ctx):
        return Response(Status.SUCCESS, "text/plain",
                        f"Hello, {what}!".encode())

    return greeting_provider

Or equivalently, using a class:

from amethyst.response import Status, Response

class GreetingProvider():
    def __init__(self, what="world"):
        self.what = what

    async def __call__(self, ctx):
        return Response(Status.SUCCESS, "text/plain",
                        f"Hello, {self.what}!".encode())

Now that you've defined a provider factory, you must register it so you can use it in configuration. Provider factory discovery is done using Python package entry points, which you can define in your "setup.py". Here is an minimal "setup.py" for this project, supposing you've saved this file as "greeting_provider.py" in its own directory:

from setuptools import find_packages, setup

setup(
    name="greeting_provider",
    py_modules=["greeting_provider"],
    entry_points={
        "amethyst.resources": [
            "greeting = greeting_provider:GreetingProvider"
        ]
    }
)

Note the "greeting = greeting_provider:GreetingProvider" line. This means we are registering a new resource type called "greeting", using the attribute "GreetingProvider" from the module "greeting_provider" to provide it.

If you had a more complex provider and shipped it as a full package instead of a module, you'd need to use "packages=find_packages()" instead of a hardcoded "py_modules" like in this example.

You can now install this directory as a package the same way you installed Amethyst. This will probably be some form of "pip install .". (On NixOS, you'll need to write a Nix expression to install your package with "buildPythonPackage", then override the propagatedBuildInputs of "services.amethyst.package" to make everything work.)

Once all this is done, create an Amethyst configuration file referencing your new resource type:

{
  "hosts": [{
    "name": "localhost",
    "paths": {
      "/greeter/": {
        "type": "greeting",
        "what": "universe"
      }
    }
  }]
}

...and browse to "gemini://localhost/greeter/" to see your provider in action!

The line '"type": "greeting"' tells Amethyst to use your new provider. Any other attributes next to this one are passed as keyword arguments directly to your provider factory.

The resource provider API

Resource providers are called with a single parameter representing the request context, as defined in "amethyst.request.Context".

They return a Response object, constructed like so:

from amethyst.response import Status, Response

def ...(...):
    return Response(Status.SUCCESS, "text/gemini", response_body)

amethyst.response API

Here are the attributes available on a Context object:

"conn" is a "amethyst.request.Connection" object with these attributes:

"server" is a "amethyst.server.Server" object; the only interesting / public property is "config" which is an "amethyst.config.Config" object. You should probably treat "config" as read-only, since it could be reloaded at any time.