decentralized and customizable links page on top of atproto ligo.at
atproto link-in-bio python uv
9
fork

Configure Feed

Select the types of activity you want to include in your feed.

move atproto to a separate workspace

+59 -54
+3
packages/atproto/pyproject.toml
··· 1 + [project] 2 + name = "atproto" 3 + version = "0.0.0"
+1 -5
packages/ingestor/pyproject.toml
··· 3 3 version = "0.0.0" 4 4 dependencies = [ 5 5 "atproto_jetstream>=0.3.0", 6 - "python-dotenv>=1.2.1", 6 + "python-dotenv", 7 7 ] 8 8 9 9 [project.scripts] 10 10 ingestor = "ingestor:main" 11 - 12 - [build-system] 13 - requires = ["uv_build>=0.10.0,<0.11.0"] 14 - build-backend = "uv_build"
-4
packages/xrpc/pyproject.toml
··· 1 1 [project] 2 2 name = "xrpc" 3 3 version = "0.0.0" 4 - 5 - [build-system] 6 - requires = ["uv_build>=0.10.0,<0.11.0"] 7 - build-backend = "uv_build"
+2
pyproject.toml
··· 11 11 "flask-htmx>=0.4.0", 12 12 "flask[async,dotenv]>=3.1.2", 13 13 "gunicorn>=25.0.3", 14 + "atproto", 14 15 "ingestor", 15 16 "xrpc", 16 17 ] 17 18 18 19 [tool.uv.sources] 20 + atproto = { workspace = true } 19 21 ingestor = { workspace = true } 20 22 xrpc = { workspace = true } 21 23
+1 -19
src/atproto/__init__.py packages/atproto/src/atproto/__init__.py
··· 8 8 from aiodns import error as dns_error 9 9 from aiohttp.client import ClientResponse, ClientSession 10 10 11 - from src.security import is_safe_url 12 - 13 11 from .kv import KV, nokv 12 + from .security import is_safe_url 14 13 from .types import DID, AuthserverUrl, Handle, PdsUrl 15 - from .validator import is_valid_authserver_meta 16 14 17 15 PLC_DIRECTORY = getenv("PLC_DIRECTORY_URL") or "https://plc.directory" 18 16 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])?$" ··· 255 253 authserver_url = parsed["authorization_servers"][0] 256 254 kv.set(pds_url, value=authserver_url) 257 255 return AuthserverUrl(authserver_url) 258 - 259 - 260 - async def fetch_authserver_meta( 261 - client: ClientSession, 262 - authserver_url: str, 263 - ) -> dict[str, str] | None: 264 - """Returns metadata from the authserver""" 265 - 266 - assert is_safe_url(authserver_url) 267 - endpoint = urljoin(authserver_url, "/.well-known/oauth-authorization-server") 268 - response = await client.get(endpoint) 269 - if not response.ok: 270 - return None 271 - meta: dict[str, Any] = await response.json() 272 - assert is_valid_authserver_meta(meta, authserver_url) 273 - return meta 274 256 275 257 276 258 async def get_record(
src/atproto/kv.py packages/atproto/src/atproto/kv.py
+19 -3
src/atproto/oauth.py packages/atproto/src/atproto/oauth.py
··· 1 1 import json 2 2 import time 3 3 from typing import Any, Callable, NamedTuple 4 + from urllib.parse import urljoin 4 5 5 6 from aiohttp.client import ClientResponse, ClientSession 6 7 from authlib.common.security import generate_token 7 8 from authlib.jose import JsonWebKey, Key, jwt 8 9 from authlib.oauth2.rfc7636 import create_s256_code_challenge 9 10 10 - from src.security import hardened_http, is_safe_url 11 - 12 - from . import fetch_authserver_meta 11 + from .security import hardened_http, is_safe_url 13 12 from .types import OAuthAuthRequest, OAuthSession 13 + from .validator import is_valid_authserver_meta 14 14 15 15 16 16 class OAuthTokens(NamedTuple): ··· 21 21 # only for parsing 22 22 token_type: str | None 23 23 expires_in: int | None 24 + 25 + 26 + async def fetch_authserver_meta( 27 + client: ClientSession, 28 + authserver_url: str, 29 + ) -> dict[str, str] | None: 30 + """Returns metadata from the authserver""" 31 + 32 + assert is_safe_url(authserver_url) 33 + endpoint = urljoin(authserver_url, "/.well-known/oauth-authorization-server") 34 + response = await client.get(endpoint) 35 + if not response.ok: 36 + return None 37 + meta: dict[str, Any] = await response.json() 38 + assert is_valid_authserver_meta(meta, authserver_url) 39 + return meta 24 40 25 41 26 42 # Prepares and sends a pushed auth request (PAR) via HTTP POST to the Authorization Server.
src/atproto/types.py packages/atproto/src/atproto/types.py
src/atproto/validator.py packages/atproto/src/atproto/validator.py
+2 -3
src/auth.py
··· 2 2 from typing import NamedTuple, TypeVar 3 3 4 4 from aiohttp.client import ClientSession 5 + from atproto.oauth import refresh_token_request 6 + from atproto.types import OAuthAuthRequest, OAuthSession 5 7 from authlib.jose import JsonWebKey 6 8 from flask import current_app, request 7 9 from flask.sessions import SessionMixin 8 - 9 - from src.atproto.oauth import refresh_token_request 10 - from src.atproto.types import OAuthAuthRequest, OAuthSession 11 10 12 11 13 12 def save_auth_request(session: SessionMixin, request: OAuthAuthRequest):
+2 -3
src/db.py
··· 3 3 from sqlite3 import Connection 4 4 from typing import Generic, Literal, cast, override 5 5 6 + from atproto.kv import KV as BaseKV 7 + from atproto.kv import K, V 6 8 from flask import Flask, g 7 - 8 - from src.atproto.kv import KV as BaseKV 9 - from src.atproto.kv import K, V 10 9 11 10 12 11 class KV(BaseKV, Generic[K, V]):
+8 -8
src/main.py
··· 3 3 from typing import Any, NamedTuple, cast 4 4 5 5 from aiohttp.client import ClientSession 6 - from flask import Flask, g, redirect, render_template, request, session, url_for 7 - from flask_htmx import HTMX 8 - from flask_htmx import make_response as htmx_response 9 - from xrpc import xrpc 10 - 11 - from src.atproto import ( 6 + from atproto import ( 12 7 get_record, 13 8 is_valid_did, 14 9 resolve_did_from_handle, 15 10 resolve_pds_from_did, 16 11 ) 17 - from src.atproto.oauth import pds_authed_req 18 - from src.atproto.types import DID, Handle, OAuthSession, PdsUrl 12 + from atproto.oauth import pds_authed_req 13 + from atproto.types import DID, Handle, OAuthSession, PdsUrl 14 + from flask import Flask, g, redirect, render_template, request, session, url_for 15 + from flask_htmx import HTMX 16 + from flask_htmx import make_response as htmx_response 17 + from xrpc import xrpc 18 + 19 19 from src.auth import ( 20 20 get_auth_session, 21 21 refresh_auth_session,
+11 -8
src/oauth.py
··· 3 3 from urllib.parse import urlencode 4 4 5 5 from aiohttp.client import ClientSession 6 - from authlib.jose import JsonWebKey, Key 7 - from flask import Blueprint, current_app, jsonify, redirect, request, session, url_for 8 - 9 - from src.atproto import ( 10 - fetch_authserver_meta, 6 + from atproto import ( 11 7 is_valid_did, 12 8 is_valid_handle, 13 9 resolve_authserver_from_pds, 14 10 resolve_identity, 15 11 ) 16 - from src.atproto.oauth import initial_token_request, send_par_auth_request 17 - from src.atproto.types import ( 12 + from atproto.oauth import ( 13 + fetch_authserver_meta, 14 + initial_token_request, 15 + send_par_auth_request, 16 + ) 17 + from atproto.security import hardened_http, is_safe_url 18 + from atproto.types import ( 18 19 DID, 19 20 AuthserverUrl, 20 21 Handle, ··· 22 23 OAuthSession, 23 24 PdsUrl, 24 25 ) 26 + from authlib.jose import JsonWebKey, Key 27 + from flask import Blueprint, current_app, jsonify, redirect, request, session, url_for 28 + 25 29 from src.auth import ( 26 30 delete_auth_request, 27 31 get_auth_request, ··· 29 33 save_auth_session, 30 34 ) 31 35 from src.db import KV, get_db 32 - from src.security import hardened_http, is_safe_url 33 36 34 37 oauth = Blueprint("oauth", __name__, url_prefix="/oauth") 35 38
+1
src/security.py packages/atproto/src/atproto/security.py
··· 1 1 from urllib.parse import urlparse 2 + 2 3 import aiohttp 3 4 4 5
+9 -1
uv.lock
··· 4 4 5 5 [manifest] 6 6 members = [ 7 + "atproto", 7 8 "ingestor", 8 9 "ligo-at", 9 10 "xrpc", ··· 101 102 wheels = [ 102 103 { 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" }, 103 104 ] 105 + 106 + [[package]] 107 + name = "atproto" 108 + version = "0.0.0" 109 + source = { editable = "packages/atproto" } 104 110 105 111 [[package]] 106 112 name = "atproto-jetstream" ··· 362 368 [package.metadata] 363 369 requires-dist = [ 364 370 { name = "atproto-jetstream", specifier = ">=0.3.0" }, 365 - { name = "python-dotenv", specifier = ">=1.2.1" }, 371 + { name = "python-dotenv" }, 366 372 ] 367 373 368 374 [[package]] ··· 393 399 dependencies = [ 394 400 { name = "aiodns" }, 395 401 { name = "aiohttp" }, 402 + { name = "atproto" }, 396 403 { name = "authlib" }, 397 404 { name = "flask", extra = ["async", "dotenv"] }, 398 405 { name = "flask-htmx" }, ··· 405 412 requires-dist = [ 406 413 { name = "aiodns", specifier = ">=4.0.0" }, 407 414 { name = "aiohttp", specifier = ">=3.13.3" }, 415 + { name = "atproto", editable = "packages/atproto" }, 408 416 { name = "authlib", specifier = ">=1.6.7" }, 409 417 { name = "flask", extras = ["async", "dotenv"], specifier = ">=3.1.2" }, 410 418 { name = "flask-htmx", specifier = ">=0.4.0" },