Metadata-Version: 2.5
Name: askwol
Version: 0.1.0
Summary: OWL ontology review and validation: class diagram, namespace and term checks, metadata and documentation review
Project-URL: Homepage, https://lod-4tu.tudelft.nl/askwol
Project-URL: Repository, https://github.com/TDCC-NES/askwol
Project-URL: Issues, https://github.com/TDCC-NES/askwol/issues
Author-email: Kathrin Füllenbach <nes@tdcc.nl>, Dani Metilli <nes@tdcc.nl>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.10
Requires-Dist: click>=8.0
Requires-Dist: defusedxml>=0.7.1
Requires-Dist: fastapi>=0.110
Requires-Dist: httpx>=0.27
Requires-Dist: owlrl<8,>=7.6.1
Requires-Dist: pydantic>=2.0
Requires-Dist: pyshacl==0.40.0
Requires-Dist: python-multipart>=0.0.9
Requires-Dist: rdflib<8,>=7.3
Requires-Dist: rich>=13.0
Requires-Dist: uvicorn[standard]>=0.29
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Description-Content-Type: text/markdown

# askwol 🦉

> **Drop in an OWL ontology - get back a class diagram, namespace and term checks, metadata review, and a clean-up report. In seconds.**

[![Licence: MIT](https://img.shields.io/badge/Licence-MIT-blue.svg)](LICENSE)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
[![Tests](https://github.com/TDCC-NES/askwol/actions/workflows/tests.yml/badge.svg)](https://github.com/TDCC-NES/askwol/actions/workflows/tests.yml)
[![Built with FastAPI](https://img.shields.io/badge/built%20with-FastAPI-009688.svg)](https://fastapi.tiangolo.com/)

👉 **Try it live:** https://lod-4tu.tudelft.nl/askwol/

<p align="center">
  <img src="docs/screenshot.png" alt="askwol web UI screenshot" width="720">
</p>

## Why askwol?

The W3C originally planned to call their Web Ontology Language **WOL**. Tim Finin [proposed rearranging it to **OWL**](http://lists.w3.org/Archives/Public/www-webont-wg/2001Dec/0169.html), since *"owls are associated with wisdom."* Fittingly, [Owl](https://en.wikipedia.org/wiki/Owl_(Winnie-the-Pooh)) from Milne's *Winnie-the-Pooh* [famously misspells his own name](https://lists.w3.org/Archives/Public/www-webont-wg/2002Sep/0301.html) as **"Wol."**

So the name went WOL → OWL → and, for a tool that *asks* Owl for a wise second opinion on your ontology, back to **askwol**. Wollie, to friends.

<p align="center">
  <a href="https://commons.wikimedia.org/wiki/File:Winnie-the-Pooh_67.png">
    <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Winnie-the-Pooh_67.png/250px-Winnie-the-Pooh_67.png" alt="Owl by E.H. Shepard (1926, public domain)" width="180">
  </a>
</p>

## What do you get?

An interactive **class diagram** of your ontology, plus a single HTML report (or JSON via the API) with one section per automated check, grouped into five areas: ontology basics, namespaces & reuse, term structure, term documentation, and logic. Every section links to a matching entry in the built-in **publishing guide** at `/guide`, so a failing check always tells you *why* the convention exists.

See the full, always-up-to-date list of checks on the [live app](https://lod-4tu.tudelft.nl/askwol/#what-do-you-get) or in the [publishing guide](https://lod-4tu.tudelft.nl/askwol/guide).

## Quick start

The fastest way (no clone needed):

```bash
pipx install git+https://github.com/TDCC-NES/askwol.git
askwol check your-ontology.ttl
```

Or for development:

```bash
git clone https://github.com/TDCC-NES/askwol.git
cd askwol
python -m venv .venv
source .venv/bin/activate   # may differ depending on your shell
pip install -e ".[dev]"   # Python 3.10+
```

## Usage

### CLI

```bash
# Rich terminal output
askwol check ontology.ttl

# Markdown / JSON report
askwol check ontology.ttl --format markdown -o report.md
askwol check ontology.ttl --format json

# Options
askwol check ontology.ttl --timeout 60        # default: 30s
askwol check ontology.ttl --skip-resolution   # parse only
```

Exit codes: `0` all pass, `1` issues found.

### Web UI

Run the Web UI locally

With the virtual environment activated:

```bash
python -m uvicorn askwol.web:app --port 8000
```

Open http://127.0.0.1:8000.

For development with automatic reloading:
```bash
python -m uvicorn askwol.web:app --reload --reload-dir src --port 8000
```

Open http://127.0.0.1:8000/. Endpoints: `GET /` (upload form), `POST /validate` (HTML report), `POST /api/validate` (JSON), `GET /guide` (publishing guide), `GET /health`, `GET /docs` (Swagger / OpenAPI).

## Deployment (Docker)

The repo ships with a `Dockerfile` and `docker-compose.yml` so that the web app can be deployed on any Linux server with Docker.

### Run locally

```bash
# build and start detached
docker compose up -d --build

# rebuild AND recreate after code changes
# (plain `--build` can reuse the old container, leaving stale code running)
docker compose up -d --build --force-recreate

# logs / stop
docker compose logs -f askwol
docker compose down
```

Then open http://localhost:8000. If the page still looks stale after a rebuild,
hard-refresh the browser (Cmd/Ctrl+Shift+R) to clear cached assets.

#### Development with hot-reload

`docker-compose.override.yml.example` bind-mounts `src/` and runs uvicorn with
`--reload`. Copy it once and Compose merges it automatically:

```bash
cp docker-compose.override.yml.example docker-compose.override.yml
docker compose up --build      # first run, and whenever deps change
# edit files under src/askwol/ - uvicorn reloads on save
```

The override file is gitignored, so it never reaches the server.

### Deploy on a server

Prerequisites: a Linux host with Docker, a domain pointing to it, and a reverse proxy such as [Caddy](https://caddyserver.com/),  [Apache mod_proxy](https://httpd.apache.org/docs/current/mod/mod_proxy.html), or [Nginx Reverse Proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/).

```bash
# on the server
git clone https://github.com/TDCC-NES/askwol.git /opt/askwol
cd /opt/askwol
# do NOT copy docker-compose.override.yml.example here - without it, the
# production CMD from the Dockerfile (2 workers, no reload) is used.
docker compose up -d --build --force-recreate
```

The container binds to `127.0.0.1:8000` only. Point your reverse proxy at it, e.g. a minimal Caddyfile:

```caddy
askwol.example.com {
    encode zstd gzip
    request_body { max_size 25MB }
    reverse_proxy 127.0.0.1:8000
}
```

Caddy will obtain a Let's Encrypt certificate automatically. Updates:

```bash
cd /opt/askwol && git pull && docker compose up -d --build --force-recreate
```

#### Serving under a sub-path

If askwol sits at the root of a domain (`https://askwol.example.com/`), nothing
extra is needed.

If you serve it under a path prefix (e.g. `https://server/askwol/`), set
`ASKWOL_ROOT_PATH` to that prefix. askwol then rewrites its internal navigation
links to include the prefix, so every internal link resolves correctly without
relying on JavaScript or a trailing slash.

```yaml
# docker-compose.override.yml, or an .env file
environment:
  - ASKWOL_ROOT_PATH=/askwol
```

Point the reverse proxy at `127.0.0.1:8000` and strip the prefix before
forwarding, e.g. with Caddy:

```caddy
server.example.com {
    handle_path /askwol/* {
        reverse_proxy 127.0.0.1:8000
    }
}
```

**Security notes:** askwol fetches arbitrary URLs (namespace resolution + URL upload). Outbound requests to private, loopback, and other internal IP ranges are blocked automatically (SSRF guard in [`resolver.py`](src/askwol/resolver.py)). Each client IP is capped at `ASKWOL_RATE_LIMIT` requests per minute (default 20; set to `0` to disable) on `/validate` and `/api/validate`. Uploads are capped at 20 MB in the app itself; also enforce a request-size limit on the reverse proxy as defence-in-depth.

**Validation limits:** each validation runs in its own isolated process, never inline in a web worker, so a slow or hung ontology can be killed without affecting other requests. A hard timeout (`ASKWOL_VALIDATION_TIMEOUT`, default 300 seconds) kills a job outright and returns a `504`. A global concurrency limit (`ASKWOL_MAX_CONCURRENT_VALIDATIONS`, default 2; keep at or below the host's CPU core count) caps how many validations run at once, and rejects excess requests immediately with a `503` instead of queueing them. Generous size caps (`ASKWOL_MAX_TRIPLES` default 500000, `ASKWOL_MAX_NAMESPACES` default 500, `ASKWOL_MAX_IMPORTS` default 200) reject oversized ontologies right after parsing, before any expensive check runs. `/validate`, `/api/validate`, and the CLI all share the same validation pipeline and the same limits. If your reverse proxy sets its own timeout for these routes, set it comfortably above `ASKWOL_VALIDATION_TIMEOUT` so askwol's own timeout response is what clients actually see.

### Usage tracking

A lightweight, privacy-friendly tracker logs each validation request to a local SQLite database. No cookies, no JavaScript, no third-party services. IPs are hashed with a per-database secret so they cannot be recovered from the stored data.

Recorded per event: timestamp, request kind (`validate`, `validate_upload`, or `validate_api`), source (the submitted URL or filename), HTTP status, duration in ms, and a truncated SHA-256 hash of the client IP.

Environment variables:

| Variable | Default | Purpose |
| --- | --- | --- |
| `ASKWOL_USAGE_DB` | `data/usage.db` | Path to the SQLite file. Local runs and the Docker setup both write here (`/data/usage.db` in the container maps to `./data/` on the host). |
| `ASKWOL_STATS_TOKEN` | *(unset)* | Required to view the `/stats` JSON dashboard. If unset, `/stats` returns 503. |
| `ASKWOL_USAGE_DISABLED` | *(unset)* | Set to `1` to disable tracking entirely. |

Enable the dashboard:

```bash
echo "ASKWOL_STATS_TOKEN=$(openssl rand -hex 32)" >> .env
docker compose up -d
curl "http://127.0.0.1:8000/stats?token=$(grep ASKWOL_STATS_TOKEN .env | cut -d= -f2)"
```

Returns aggregated usage counts: total events, unique IP hashes, and events per day (all-time), plus the most-validated sources, split into URLs and uploaded files.

### Python API

```python
import asyncio
from askwol.parser import parse_ontology
from askwol.cache import OntologyCache
from askwol.resolver import resolve_all_namespaces
from askwol.term_validator import validate_terms

parsed = parse_ontology("ontology.ttl")
cache = OntologyCache()
checks = asyncio.run(resolve_all_namespaces(parsed.namespaces, cache))

for prefix, uri in parsed.namespaces.items():
    for r in validate_terms(prefix, uri, parsed.terms_by_namespace.get(prefix, set()), cache):
        print(f"{r.prefix}:{r.local_name} -> {r.status}")
```

## Supported formats

Turtle (`.ttl`), RDF/XML (`.rdf`, `.owl`), JSON-LD (`.jsonld`), N-Triples (`.nt`), N3 (`.n3`)

## Tests

```bash
pytest tests/ -v
```

The test suite covers every check on good and bad inputs, HTML report
rendering, and the FastAPI routes via `TestClient`, plus a pinned smoke test
on [`html/ontologies/broken.ttl`](html/ontologies/broken.ttl) that fails if any
check ever stops detecting issues (clean counterpart:
[`html/ontologies/sample.ttl`](html/ontologies/sample.ttl)). Drop either into
the upload form at http://localhost:8000/ to see a full report.

## Licence

MIT - see [LICENSE](LICENSE).
