Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
OPENSYNDROME_PROVIDER=ollama
OPENSYNDROME_MODEL=mistral
# optional: directory with your own definitions, read alongside the community ones
# OPENSYNDROME_DEFINITIONS_DIR=./my-definitions
# optional: ignore the community definitions and use OPENSYNDROME_DEFINITIONS_DIR only
# OPENSYNDROME_LOCAL_DEFINITIONS_ONLY=1
# mandatory only if using Ollama
OLLAMA_BASE_URL=http://localhost:11434
# optional: add the API KEY of your desired provider in your .env file
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,33 @@ opensyndrome download definitions

The files will be placed in the folder `.open_syndrome` in `$HOME`.

### Using your own local definitions

The definitions published by the Open Syndrome Initiative (the community definitions) always live in
`~/.open_syndrome/v1/definitions`. To add definitions of your own that are not published on GitHub, point
`OPENSYNDROME_DEFINITIONS_DIR` to a directory you maintain; it is never written to by the CLI.

```bash
OPENSYNDROME_DEFINITIONS_DIR=./my-definitions
```

To ignore the community definitions altogether and use only yours, also set
`OPENSYNDROME_LOCAL_DEFINITIONS_ONLY=1` (`true`, `yes` and `on` also work). Nothing is downloaded in that
mode, not even with `--force`.

From Python, `opensyndrome.artifacts.get_definition_dirs()` returns the directories to read, in order:

| `OPENSYNDROME_DEFINITIONS_DIR` | `OPENSYNDROME_LOCAL_DEFINITIONS_ONLY` | Result |
|---|---|---|
| unset | unset | `[community]` |
| set | unset | `[community, local]` |
| set | set | `[local]` |
| unset | set | error |

It also accepts `local_dir` and `local_only` keyword arguments that override the environment variables.
The former `get_definition_dir()` still returns the community directory only, ignoring these variables;
it emits a `DeprecationWarning` and will be removed in a future version.

### Providers and configuration

The provider and model can be set via environment variables so you don't have to pass them on every command:
Expand Down
99 changes: 95 additions & 4 deletions opensyndrome/artifacts.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import json
import os
import warnings
from pathlib import Path

from dotenv import load_dotenv
import requests


Expand All @@ -9,6 +13,35 @@
SCHEMA_DIR = OPEN_SYNDROME_DIR / "schema.json"
DEFINITIONS_DIR = OPEN_SYNDROME_DIR / "definitions"
DEFINITIONS_DIR.mkdir(parents=True, exist_ok=True)
DEFINITIONS_DIR_ENV_VAR = "OPENSYNDROME_DEFINITIONS_DIR"
LOCAL_DEFINITIONS_ONLY_ENV_VAR = "OPENSYNDROME_LOCAL_DEFINITIONS_ONLY"
_TRUTHY_VALUES = {"1", "true", "yes", "on"}

load_dotenv()


def local_definitions_dir() -> Path | None:
"""Directory with the user's own definitions, from ``OPENSYNDROME_DEFINITIONS_DIR``.

Returns ``None`` when the variable is unset or empty. The directory is owned
by the user and never written to; it must already exist.
"""
value = os.environ.get(DEFINITIONS_DIR_ENV_VAR, "").strip()
if not value:
return None
directory = Path(value).expanduser().resolve()
if not directory.is_dir():
raise FileNotFoundError(
f"{DEFINITIONS_DIR_ENV_VAR} points to a directory that does not exist: "
f"{directory}"
)
return directory


def local_definitions_only() -> bool:
"""Whether ``OPENSYNDROME_LOCAL_DEFINITIONS_ONLY`` asks to ignore the community definitions."""
value = os.environ.get(LOCAL_DEFINITIONS_ONLY_ENV_VAR, "")
return value.strip().lower() in _TRUTHY_VALUES


def download_schema():
Expand All @@ -20,10 +53,14 @@ def download_schema():
return SCHEMA_DIR


def download_definitions(url=None, current_path=None):
"""Download definitions from GitHub repos."""
def download_definitions(
url: str | None = None, current_path: Path | None = None
) -> None:
"""Download the community definitions from GitHub into ``DEFINITIONS_DIR``."""
if url is None:
url = "https://api.github.com/repos/OpenSyndrome/definitions/contents/definitions/v1?ref=main"
if current_path is None:
current_path = DEFINITIONS_DIR
response = requests.get(url)
response.raise_for_status()

Expand All @@ -37,15 +74,69 @@ def download_definitions(url=None, current_path=None):
print(item["path"])
local_dir = DEFINITIONS_DIR / Path(item["path"]).parts[-1]
local_dir.mkdir(parents=True, exist_ok=True)
download_definitions(item["url"], local_dir)
download_definitions(url=item["url"], current_path=local_dir)


def get_definition_dirs(
force: bool = False,
*,
local_dir: str | Path | None = None,
local_only: bool | None = None,
) -> list[Path]:
"""Return the directories holding definitions, in lookup order.

- Neither environment variable set: ``[community]``, downloading the
community definitions when the directory is empty or ``force`` is set.
- ``OPENSYNDROME_DEFINITIONS_DIR`` set: ``[community, local]``.
- ``OPENSYNDROME_LOCAL_DEFINITIONS_ONLY`` set: ``[local]``, never downloading.

def get_definition_dir(force=False):
``local_dir`` and ``local_only`` override the respective environment
variables when given.
"""
if local_dir is None:
local_dir = local_definitions_dir()
else:
local_dir = Path(local_dir).expanduser().resolve()
if local_only is None:
local_only = local_definitions_only()

if local_only:
if local_dir is None:
raise ValueError(
f"{LOCAL_DEFINITIONS_ONLY_ENV_VAR} is set, so "
f"{DEFINITIONS_DIR_ENV_VAR} must be set to a directory with your "
"local definitions."
)
return [local_dir]

community_dir = _community_definitions_dir(force)
if local_dir is None:
return [community_dir]
return [community_dir, local_dir]


def _community_definitions_dir(force: bool) -> Path:
"""Return the community directory, downloading into it when empty or forced."""
if force or not any(DEFINITIONS_DIR.iterdir()):
download_definitions()
return DEFINITIONS_DIR


def get_definition_dir(force: bool = False) -> Path:
"""Deprecated: return the community definitions directory only.

Kept so existing callers keep working. It ignores the local definitions
environment variables; use :func:`get_definition_dirs` to honour them.
"""
warnings.warn(
"get_definition_dir is deprecated and will be removed in a future version; "
"use get_definition_dirs, which also returns your local definitions.",
DeprecationWarning,
stacklevel=2,
)
return _community_definitions_dir(force)


def get_schema_filepath(force=False):
if force or not SCHEMA_DIR.exists():
download_schema()
Expand Down
47 changes: 34 additions & 13 deletions opensyndrome/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
generate_human_readable_format,
)
from opensyndrome.ontology import enrich_definition, MAPPERS
from opensyndrome.artifacts import get_schema_filepath, get_definition_dir
from opensyndrome.artifacts import (
LOCAL_DEFINITIONS_ONLY_ENV_VAR,
get_schema_filepath,
get_definition_dirs,
local_definitions_only,
)
from opensyndrome.validators import validate_machine_readable_format
from opensyndrome.providers import (
build_model_string,
Expand Down Expand Up @@ -284,21 +289,37 @@ def convert_to_text(json_file, model, language, provider):
def download_entity(entity, force):
match entity:
case "schema":
result = get_schema_filepath(force=force)
click.echo(
click.style(
f"{entity} available at: {get_schema_filepath(force=force)}",
fg="green",
)
)
case "definitions":
result = get_definition_dir(force=force)
if local_definitions_only():
click.echo(
click.style(
f"{LOCAL_DEFINITIONS_ONLY_ENV_VAR} is set: skipping download, "
"using local definitions only.",
fg="yellow",
)
)
try:
dirs = get_definition_dirs(force=force)
except (ValueError, FileNotFoundError) as e:
click.echo(click.style(f"❌ {e}", fg="red"), err=True)
raise SystemExit(1)
for directory in dirs:
click.echo(
click.style(f"{entity} available at: {directory}", fg="green")
)
case _:
result = None

if not result:
click.echo(
click.style(
f"Invalid entity: {entity}. Expected: `schema` or `definitions`.",
fg="red",
click.echo(
click.style(
f"Invalid entity: {entity}. Expected: `schema` or `definitions`.",
fg="red",
)
)
)
else:
click.echo(click.style(f"{entity} available at: {result}", fg="green"))


def main():
Expand Down
Loading
Loading