Metadata-Version: 2.4
Name: linkified
Version: 0.1.0
Summary: Framework-agnostic Python package for shareable resource access control
Author-email: mcjraquel <celyn@meddicc.com>
License: MIT
License-File: LICENSE
Requires-Python: >=3.11
Provides-Extra: all
Requires-Dist: anyio>=3.7; extra == 'all'
Requires-Dist: argon2-cffi>=23.1; extra == 'all'
Requires-Dist: django>=4.2; extra == 'all'
Requires-Dist: fastapi>=0.100.0; extra == 'all'
Requires-Dist: flask>=3.0; extra == 'all'
Requires-Dist: pyjwt>=2.8; extra == 'all'
Requires-Dist: redis>=5.0; extra == 'all'
Requires-Dist: sqlalchemy>=2.0; extra == 'all'
Provides-Extra: argon2
Requires-Dist: argon2-cffi>=23.1; extra == 'argon2'
Provides-Extra: dev
Requires-Dist: anyio[trio]>=3.7; extra == 'dev'
Requires-Dist: argon2-cffi>=23.1; extra == 'dev'
Requires-Dist: django>=4.2; extra == 'dev'
Requires-Dist: djangorestframework>=3.15; extra == 'dev'
Requires-Dist: fakeredis>=2.21; extra == 'dev'
Requires-Dist: fastapi>=0.100.0; extra == 'dev'
Requires-Dist: flask>=3.0; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pyjwt>=2.8; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest-django>=4.8; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: redis>=5.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Requires-Dist: sqlalchemy>=2.0; extra == 'dev'
Provides-Extra: django
Requires-Dist: django>=4.2; extra == 'django'
Provides-Extra: fastapi
Requires-Dist: anyio>=3.7; extra == 'fastapi'
Requires-Dist: fastapi>=0.100.0; extra == 'fastapi'
Provides-Extra: flask
Requires-Dist: flask>=3.0; extra == 'flask'
Provides-Extra: jwt
Requires-Dist: pyjwt>=2.8; extra == 'jwt'
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == 'redis'
Provides-Extra: sqlalchemy
Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
Description-Content-Type: text/markdown

# Linkified

**Framework-agnostic Python package for shareable resource access control.**

Linkified provides a complete system for creating, managing, validating, and revoking shareable links for application resources — with pluggable storage backends and first-class integrations for FastAPI, Django, and Flask.

```python
from linkified import LinkifiedManager, MemoryBackend

share = LinkifiedManager(backend=MemoryBackend())

token, link = share.create("document:123", permissions=["read"])
# → token is a URL-safe string to embed in a share URL
# → link.id is the stable identifier for lifecycle management

access = share.validate(token)
# → ShareAccess(resource="document:123", permissions=["read"], ...)

share.revoke(link.id)
```

---

## Installation

```bash
pip install linkified
```

With optional extras:

```bash
pip install "linkified[fastapi]"
pip install "linkified[django]"
pip install "linkified[flask]"
pip install "linkified[redis]"
pip install "linkified[sqlalchemy]"
pip install "linkified[all]"
```

---

## Quickstart

### FastAPI

```python
from fastapi import FastAPI, Depends
from linkified import LinkifiedManager
from linkified.backends import MemoryBackend
from linkified.frameworks.fastapi import require_share

app = FastAPI()
share = LinkifiedManager(backend=MemoryBackend())
app.state.linkified = share

@app.get("/documents/{id}")
async def view_document(id: int, access=Depends(require_share("read", manager=share))):
    return {"resource": access.resource}
```

### Django

```python
# settings.py
from linkified import LinkifiedManager
from linkified.backends import SQLAlchemyBackend

LINKIFIED_MANAGER = LinkifiedManager(backend=SQLAlchemyBackend(url="postgresql+psycopg://..."))

# views.py
from linkified.frameworks.django import share_only

@share_only("read")
def view_document(request, doc_id, share_access=None):
    return JsonResponse({"resource": share_access.resource})
```

### Flask

```python
from flask import Flask, g, jsonify
from linkified import LinkifiedManager
from linkified.backends import MemoryBackend
from linkified.frameworks.flask import Linkified, share_only

app = Flask(__name__)
ext = Linkified(manager=LinkifiedManager(backend=MemoryBackend()))
ext.init_app(app)

@app.get("/documents/<int:doc_id>")
@share_only("read")
def view_document(doc_id):
    return jsonify({"resource": g.share_access.resource})
```

---

## Storage Backends

| Backend | Use case |
|---|---|
| `MemoryBackend` | Testing / development |
| `SQLAlchemyBackend` | Persistent storage, auditing |
| `RedisBackend` | High-performance, TTL expiry |
| `HybridBackend` | PostgreSQL + Redis (recommended for production) |
| `JWTBackend` | Stateless, no storage required (no revocation) |

```python
from linkified.backends import SQLAlchemyBackend, HybridBackend
from linkified.backends.redis import RedisBackend

# Recommended production setup
share = LinkifiedManager(
    backend=HybridBackend(
        storage=SQLAlchemyBackend(url="postgresql+psycopg://..."),
        cache=RedisBackend(url="redis://localhost:6379/0"),
    )
)
```

---

## Core API

```python
# Create
token, link = share.create(
    resource="document:123",      # string or registered model instance
    permissions=["read"],
    actor=user,                   # optional; enforces policy
    expires_in=timedelta(days=7),
    single_use=True,
    password="s3cret",
    access_mode="authenticated",
    allowed_users=["user_a", "user_b"],
)

# Validate
access = share.validate(token, actor="user_a", password="s3cret")

# Lifecycle
link = share.get(link_id)
links = share.list(resource="document:123")
share.update(link_id, permissions=["read", "write"])
share.revoke(link_id)
share.restore(link_id)
share.extend(link_id, expires_in=timedelta(days=30))
share.delete(link_id)

# Audit + stats
share.access_log(link_id)
share.stats()
share.list_expiring(days=7)
share.list_by_permission("write")
```

---

## Resource Registration

```python
from linkified import shareable_resource
from linkified.policies import OwnerPolicy

@shareable_resource(
    resource_type="document",
    owner_field="owner",
    policy=OwnerPolicy,
    allow_anonymous=True,
)
class Document:
    ...
```

---

## Authorization Decorators

| Decorator | Behavior |
|---|---|
| `@share_only("read")` | Share link is the only access mechanism |
| `@share_or_permission("docs.view")` | Share link OR app permission |
| `@share_and_permission("docs.view")` | Both share link AND app permission required |

---

## License

MIT
