diff --git a/packages/atproto/pyproject.toml b/packages/atproto/pyproject.toml new file mode 100644 index 0000000..2340018 --- /dev/null +++ b/packages/atproto/pyproject.toml @@ -0,0 +1,3 @@ +[project] +name = "atproto" +version = "0.0.0" diff --git a/src/atproto/__init__.py b/packages/atproto/src/atproto/__init__.py similarity index 93% rename from src/atproto/__init__.py rename to packages/atproto/src/atproto/__init__.py index 5af0f32..6d01e9a 100644 --- a/src/atproto/__init__.py +++ b/packages/atproto/src/atproto/__init__.py @@ -8,11 +8,9 @@ from aiodns import DNSResolver from aiodns import error as dns_error from aiohttp.client import ClientResponse, ClientSession -from src.security import is_safe_url - from .kv import KV, nokv +from .security import is_safe_url from .types import DID, AuthserverUrl, Handle, PdsUrl -from .validator import is_valid_authserver_meta PLC_DIRECTORY = getenv("PLC_DIRECTORY_URL") or "https://plc.directory" HANDLE_REGEX = r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$" @@ -257,22 +255,6 @@ async def resolve_authserver_from_pds( return AuthserverUrl(authserver_url) -async def fetch_authserver_meta( - client: ClientSession, - authserver_url: str, -) -> dict[str, str] | None: - """Returns metadata from the authserver""" - - assert is_safe_url(authserver_url) - endpoint = urljoin(authserver_url, "/.well-known/oauth-authorization-server") - response = await client.get(endpoint) - if not response.ok: - return None - meta: dict[str, Any] = await response.json() - assert is_valid_authserver_meta(meta, authserver_url) - return meta - - async def get_record( client: ClientSession, pds: str, diff --git a/src/atproto/kv.py b/packages/atproto/src/atproto/kv.py similarity index 100% rename from src/atproto/kv.py rename to packages/atproto/src/atproto/kv.py diff --git a/src/atproto/oauth.py b/packages/atproto/src/atproto/oauth.py similarity index 95% rename from src/atproto/oauth.py rename to packages/atproto/src/atproto/oauth.py index a8acc3b..7aebef8 100644 --- a/src/atproto/oauth.py +++ b/packages/atproto/src/atproto/oauth.py @@ -1,16 +1,16 @@ import json import time from typing import Any, Callable, NamedTuple +from urllib.parse import urljoin from aiohttp.client import ClientResponse, ClientSession from authlib.common.security import generate_token from authlib.jose import JsonWebKey, Key, jwt from authlib.oauth2.rfc7636 import create_s256_code_challenge -from src.security import hardened_http, is_safe_url - -from . import fetch_authserver_meta +from .security import hardened_http, is_safe_url from .types import OAuthAuthRequest, OAuthSession +from .validator import is_valid_authserver_meta class OAuthTokens(NamedTuple): @@ -23,6 +23,22 @@ class OAuthTokens(NamedTuple): expires_in: int | None +async def fetch_authserver_meta( + client: ClientSession, + authserver_url: str, +) -> dict[str, str] | None: + """Returns metadata from the authserver""" + + assert is_safe_url(authserver_url) + endpoint = urljoin(authserver_url, "/.well-known/oauth-authorization-server") + response = await client.get(endpoint) + if not response.ok: + return None + meta: dict[str, Any] = await response.json() + assert is_valid_authserver_meta(meta, authserver_url) + return meta + + # Prepares and sends a pushed auth request (PAR) via HTTP POST to the Authorization Server. # Returns "state" id HTTP response on success, without checking HTTP response status async def send_par_auth_request( diff --git a/src/security.py b/packages/atproto/src/atproto/security.py similarity index 99% rename from src/security.py rename to packages/atproto/src/atproto/security.py index dd2d3c6..5d40d97 100644 --- a/src/security.py +++ b/packages/atproto/src/atproto/security.py @@ -1,4 +1,5 @@ from urllib.parse import urlparse + import aiohttp diff --git a/src/atproto/types.py b/packages/atproto/src/atproto/types.py similarity index 100% rename from src/atproto/types.py rename to packages/atproto/src/atproto/types.py diff --git a/src/atproto/validator.py b/packages/atproto/src/atproto/validator.py similarity index 100% rename from src/atproto/validator.py rename to packages/atproto/src/atproto/validator.py diff --git a/packages/ingestor/pyproject.toml b/packages/ingestor/pyproject.toml index e21078a..72baf94 100644 --- a/packages/ingestor/pyproject.toml +++ b/packages/ingestor/pyproject.toml @@ -3,12 +3,8 @@ name = "ingestor" version = "0.0.0" dependencies = [ "atproto_jetstream>=0.3.0", - "python-dotenv>=1.2.1", + "python-dotenv", ] [project.scripts] ingestor = "ingestor:main" - -[build-system] -requires = ["uv_build>=0.10.0,<0.11.0"] -build-backend = "uv_build" diff --git a/packages/xrpc/pyproject.toml b/packages/xrpc/pyproject.toml index bb0572c..699eff8 100644 --- a/packages/xrpc/pyproject.toml +++ b/packages/xrpc/pyproject.toml @@ -1,7 +1,3 @@ [project] name = "xrpc" version = "0.0.0" - -[build-system] -requires = ["uv_build>=0.10.0,<0.11.0"] -build-backend = "uv_build" diff --git a/pyproject.toml b/pyproject.toml index bd631fc..73aa294 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,11 +11,13 @@ dependencies = [ "flask-htmx>=0.4.0", "flask[async,dotenv]>=3.1.2", "gunicorn>=25.0.3", + "atproto", "ingestor", "xrpc", ] [tool.uv.sources] +atproto = { workspace = true } ingestor = { workspace = true } xrpc = { workspace = true } diff --git a/src/auth.py b/src/auth.py index ea6d33c..c4c4c92 100644 --- a/src/auth.py +++ b/src/auth.py @@ -2,13 +2,12 @@ from datetime import datetime, timedelta, timezone from typing import NamedTuple, TypeVar from aiohttp.client import ClientSession +from atproto.oauth import refresh_token_request +from atproto.types import OAuthAuthRequest, OAuthSession from authlib.jose import JsonWebKey from flask import current_app, request from flask.sessions import SessionMixin -from src.atproto.oauth import refresh_token_request -from src.atproto.types import OAuthAuthRequest, OAuthSession - def save_auth_request(session: SessionMixin, request: OAuthAuthRequest): return _set_into_session(session, "oauth_auth_request", request) diff --git a/src/db.py b/src/db.py index 96752e7..af80559 100644 --- a/src/db.py +++ b/src/db.py @@ -3,11 +3,10 @@ from logging import Logger from sqlite3 import Connection from typing import Generic, Literal, cast, override +from atproto.kv import KV as BaseKV +from atproto.kv import K, V from flask import Flask, g -from src.atproto.kv import KV as BaseKV -from src.atproto.kv import K, V - class KV(BaseKV, Generic[K, V]): db: Connection diff --git a/src/main.py b/src/main.py index e0fd009..36296d2 100644 --- a/src/main.py +++ b/src/main.py @@ -3,19 +3,19 @@ import json from typing import Any, NamedTuple, cast from aiohttp.client import ClientSession -from flask import Flask, g, redirect, render_template, request, session, url_for -from flask_htmx import HTMX -from flask_htmx import make_response as htmx_response -from xrpc import xrpc - -from src.atproto import ( +from atproto import ( get_record, is_valid_did, resolve_did_from_handle, resolve_pds_from_did, ) -from src.atproto.oauth import pds_authed_req -from src.atproto.types import DID, Handle, OAuthSession, PdsUrl +from atproto.oauth import pds_authed_req +from atproto.types import DID, Handle, OAuthSession, PdsUrl +from flask import Flask, g, redirect, render_template, request, session, url_for +from flask_htmx import HTMX +from flask_htmx import make_response as htmx_response +from xrpc import xrpc + from src.auth import ( get_auth_session, refresh_auth_session, diff --git a/src/oauth.py b/src/oauth.py index b529ad1..148513d 100644 --- a/src/oauth.py +++ b/src/oauth.py @@ -3,18 +3,19 @@ from datetime import datetime, timedelta, timezone from urllib.parse import urlencode from aiohttp.client import ClientSession -from authlib.jose import JsonWebKey, Key -from flask import Blueprint, current_app, jsonify, redirect, request, session, url_for - -from src.atproto import ( - fetch_authserver_meta, +from atproto import ( is_valid_did, is_valid_handle, resolve_authserver_from_pds, resolve_identity, ) -from src.atproto.oauth import initial_token_request, send_par_auth_request -from src.atproto.types import ( +from atproto.oauth import ( + fetch_authserver_meta, + initial_token_request, + send_par_auth_request, +) +from atproto.security import hardened_http, is_safe_url +from atproto.types import ( DID, AuthserverUrl, Handle, @@ -22,6 +23,9 @@ from src.atproto.types import ( OAuthSession, PdsUrl, ) +from authlib.jose import JsonWebKey, Key +from flask import Blueprint, current_app, jsonify, redirect, request, session, url_for + from src.auth import ( delete_auth_request, get_auth_request, @@ -29,7 +33,6 @@ from src.auth import ( save_auth_session, ) from src.db import KV, get_db -from src.security import hardened_http, is_safe_url oauth = Blueprint("oauth", __name__, url_prefix="/oauth") diff --git a/uv.lock b/uv.lock index be43acc..1446704 100644 --- a/uv.lock +++ b/uv.lock @@ -4,6 +4,7 @@ requires-python = ">=3.14" [manifest] members = [ + "atproto", "ingestor", "ligo-at", "xrpc", @@ -102,6 +103,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, ] +[[package]] +name = "atproto" +version = "0.0.0" +source = { editable = "packages/atproto" } + [[package]] name = "atproto-jetstream" version = "0.3.0" @@ -362,7 +368,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "atproto-jetstream", specifier = ">=0.3.0" }, - { name = "python-dotenv", specifier = ">=1.2.1" }, + { name = "python-dotenv" }, ] [[package]] @@ -393,6 +399,7 @@ source = { virtual = "." } dependencies = [ { name = "aiodns" }, { name = "aiohttp" }, + { name = "atproto" }, { name = "authlib" }, { name = "flask", extra = ["async", "dotenv"] }, { name = "flask-htmx" }, @@ -405,6 +412,7 @@ dependencies = [ requires-dist = [ { name = "aiodns", specifier = ">=4.0.0" }, { name = "aiohttp", specifier = ">=3.13.3" }, + { name = "atproto", editable = "packages/atproto" }, { name = "authlib", specifier = ">=1.6.7" }, { name = "flask", extras = ["async", "dotenv"], specifier = ">=3.1.2" }, { name = "flask-htmx", specifier = ">=0.4.0" },