Metadata-Version: 2.4
Name: django_i3version
Version: 0.0.2
Summary: Reusable Django app to track release versions and notify users when a new version is published.
Home-page: https://github.com/sajlx/django-i3version
Author: Ivan Bettarini
Author-email: ivan.bettarini@gmail.com
License: GNU General Public License v3.0
Classifier: Environment :: Web Environment
Classifier: Operating System :: OS Independent
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Programming Language :: Python :: 3
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: django<5.0,>=4.2
Requires-Dist: djangorestframework<4.0,>=3.14
Requires-Dist: django-simple-history<4.0,>=3.0
Provides-Extra: openapi
Requires-Dist: drf-spectacular>=0.27; extra == "openapi"
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: license-file
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: summary

# django-i3version

Reusable Django app to track **release versions / changelogs** of an
application, notify users when a new version is published, and accept
release publications from external systems (CI, scripts, Claude, …) via a
hashed API key.

```
pip install django-i3version
```

---

## Quick start

### 1. Install + add to `INSTALLED_APPS`

```python
INSTALLED_APPS = [
    ...,
    "rest_framework",
    "simple_history",        # required dependency, used for history tracking
    "django_i3version",
]
```

> **Note on app label.** The package module is `django_i3version` but the
> Django app label is intentionally set to `i3version`, so DB tables stay as
> `i3version_*`. This makes it possible to drop in this package as a
> replacement for a project that previously had a local `i3version/` Django
> app — no DB rename, no `--fake-initial`, no FK-string rewrite.

### 2. Include URLs

```python
# urls.py
from django.urls import path, include

urlpatterns = [
    ...,
    path("api/i3version/", include("django_i3version.urls")),
]
```

This mounts two viewsets under your prefix:

- `api/i3version/`           → `UserVersionViewSet` (per-user notification feed, login-based)
- `api/i3version/publisher/` → `PublisherVersionViewSet` (machine-to-machine, API-key)

### 3. Run migrations

```
python manage.py migrate i3version
```

---

## Models

- **`Version`** — a release row. Fields: `numeric_version`, `str_version`,
  `uuid_version`, `name`, `tier` (`local`/`stage`/`prod`), `short_description`,
  `description`, `markdown`, `microservice_commits` (JSON), and the user-group
  filters `user_group_filter` / `user_group_exclude` used at creation time to
  fan out `VersionUserNotified` rows.
- **`VersionAttachment`** — files attached to a version.
- **`VersionUserNotified`** — per-user notification record
  (`is_notified`, `is_read`). Created automatically on `Version.save()` for
  every user matching the version's filter. Marked read via the user API.
- **`VersionPublisher`** — holds a SHA-256-hashed API key. Authenticates
  machine-to-machine requests against the publisher endpoint.

## Settings

| Setting | Type | Default | Effect |
|---------|------|---------|--------|
| `TIER`  | str  | `None`  | If set, the user-version viewset filters only versions of this tier. |

## End-user API

Mounted at the prefix you chose in `urls.py`. Authentication uses the host
project's session/login.

| Method | Path                         | Effect |
|--------|------------------------------|--------|
| GET    | `/`                          | List versions visible to the authenticated user |
| GET    | `/{id}/`                     | Retrieve a single version |
| POST   | `/{id}/mark_as_read/`        | Mark this version as read for the user |
| POST   | `/mark_all_as_read/`         | Mark all versions as read for the user |

## Publisher API (API-key)

Mounted at `<prefix>/publisher/`. Auth header:

```
Authorization: Bearer i3v_pk_<random>
```

Provision a key via the Django admin: open **Version publishers**, create
a row, then run the **"(Re)generate API key"** admin action. The raw key
is shown **once** in the admin success message — the database only stores
its SHA-256 hash. Set `is_active=False` to revoke without deleting.

| Method | Path                | Effect |
|--------|---------------------|--------|
| GET    | `/publisher/`       | List versions |
| GET    | `/publisher/{id}/`  | Retrieve a version |
| POST   | `/publisher/`       | **Publish a new version** (creates a `Version` row and fans out `VersionUserNotified`) |

POST body example:

```json
{
  "numeric_version": "1.4.0",
  "str_version": "1.4.0",
  "name": "Spring rollout",
  "tier": "prod",
  "short_description": "...",
  "markdown": "...",
  "microservice_commits": {"backend": "abc123", "frontend": "def456"},
  "user_group_filter": {"is_active": true},
  "user_group_exclude": {"is_staff": true}
}
```

curl:

```bash
curl -X POST https://yourhost/api/i3version/publisher/ \
  -H 'Authorization: Bearer i3v_pk_xxx' \
  -H 'Content-Type: application/json' \
  -d '{"numeric_version":"1.4.0","str_version":"1.4.0","tier":"prod"}'
```

> **Security.** `user_group_filter` and `user_group_exclude` are passed
> verbatim as ORM `**kwargs` to `User.objects.filter()/exclude()` inside
> `Version.save()`. Treat publisher API keys as **trusted** — never expose
> the publisher endpoint to end users.

## OpenAPI schema

A pre-generated schema is checked into the repo at the project root for
client-codegen convenience:

- [`openapi.yaml`](./openapi.yaml)
- [`openapi.json`](./openapi.json)

The same files are also bundled inside the sdist on PyPI. Consumers can
feed either directly into `openapi-generator-cli` without booting Django.

If you want the schema rendered live by your host project, install the
optional extra and mount drf-spectacular's URLs:

```
pip install "django-i3version[openapi]"
```

```python
# settings.py
INSTALLED_APPS += ["drf_spectacular"]
REST_FRAMEWORK = {"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema"}
```

To regenerate the committed files after a change:

```
make schema
```

## Roadmap

- Webhooks on new release.
- Pluggable notification backends (Slack, email, Pub/Sub).
- Per-publisher tier scoping.
- Dedicated `I3VERSION_*` settings (key prefix override, fan-out toggle, …).

## Development

```
make virtualenv_create
source venv/bin/activate
make install_dev
make test            # runs unit tests against in-memory sqlite
make schema          # regenerate openapi.yaml + openapi.json
make package_build   # build sdist + wheel
make package_upload  # twine upload
```

---

License: GNU GPL v3.0.  
Repo: https://github.com/sajlx/django-i3version
