From 9c08e2755ef631b8b03903ede34a75f70e9cf3ed Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 15:36:18 +0000 Subject: [PATCH 1/3] Publish to PyPI as `conflens` on release - pyproject: rename the distribution to `conflens` (import name/CLI unchanged), so `pip install conflens`; uv.lock re-locked. - release.yml: after the existing version bump + tag, build (`uv build`) and publish to PyPI with twine (`__token__` + PYPI_API_TOKEN secret), gated on a `released` output so it only runs when a new version was actually cut. Publishing lives in the release job because GITHUB_TOKEN-pushed tags don't trigger a separate tag-based workflow. `--skip-existing` keeps re-runs safe. - README: document `pip install conflens` and the PYPI_API_TOKEN secret. --- .github/workflows/release.yml | 26 ++++++++++++++++++++++-- README.md | 21 +++++++++++++++---- pyproject.toml | 2 +- uv.lock | 38 +++++++++++++++++------------------ 4 files changed, 61 insertions(+), 26 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be17825..e25d957 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,12 +1,19 @@ name: Release # When a pull request is merged into main, bump the version in pyproject.toml -# (and keep uv.lock in sync), commit it back to main, and create a matching tag. +# (and keep uv.lock in sync), commit it back to main, create a matching tag, +# then build and publish the package to PyPI. # # Bump level is patch by default; add a "minor" or "major" label to the PR to # bump those instead. The version-bump commit and tag are pushed with the # built-in GITHUB_TOKEN, which by design does NOT re-trigger workflows — so this -# never loops (the `[skip ci]` marker is belt-and-suspenders). +# never loops (the `[skip ci]` marker is belt-and-suspenders). Publishing runs +# in this same job (a separate tag-triggered workflow would never fire, since +# GITHUB_TOKEN-pushed tags don't trigger workflows). +# +# PyPI publishing requires a repo secret PYPI_API_TOKEN (a PyPI API token; set +# TWINE_USERNAME=__token__). Without it the publish step fails; the bump/tag +# still succeed. on: pull_request: types: [closed] @@ -49,6 +56,7 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Bump version and create tag + id: bump run: | set -euo pipefail CUR=$(grep -m1 -E '^version *= *"' pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/') @@ -64,6 +72,7 @@ jobs: if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then echo "Tag ${TAG} already exists — nothing to do." + echo "released=false" >> "$GITHUB_OUTPUT" exit 0 fi @@ -75,4 +84,17 @@ jobs: git tag -a "${TAG}" -m "Release ${TAG} (PR #${PR_NUMBER}: ${PR_TITLE})" git push origin HEAD:main git push origin "${TAG}" + echo "released=true" >> "$GITHUB_OUTPUT" echo "Released ${TAG}" + + - name: Build distributions + if: steps.bump.outputs.released == 'true' + run: uv build # builds from the just-bumped pyproject.toml -> dist/* + + - name: Publish to PyPI + if: steps.bump.outputs.released == 'true' + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + # --skip-existing keeps re-runs idempotent if the version is already up. + run: uvx twine upload --non-interactive --skip-existing dist/* diff --git a/README.md b/README.md index e4d33d9..c06452d 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,14 @@ A desktop-style web app (built with [NiceGUI](https://nicegui.io)) that: ## Quick start -Requires Python 3.13+ and [uv](https://docs.astral.sh/uv/). +Install from PyPI (Python 3.13+) and run: + +```bash +pip install conflens +conflens # → http://localhost:6868 +``` + +Or from a clone, with [uv](https://docs.astral.sh/uv/): ```bash uv sync # Claude, OpenAI, LiteLLM work out of the box @@ -294,7 +301,13 @@ stages use a fake client), so they're fast and deterministic. Releases are automated (`.github/workflows/release.yml`): when a PR is **merged into `main`**, the workflow bumps the version in `pyproject.toml`, commits it -back to `main`, and creates a matching `vX.Y.Z` **tag**. The bump is a **patch** -by default — add a **`minor`** or **`major`** label to the PR to bump those -instead. +back to `main`, creates a matching `vX.Y.Z` **tag**, then **builds and publishes +the package to [PyPI](https://pypi.org/project/conflens/)** (`pip install +conflens`). The bump is a **patch** by default — add a **`minor`** or +**`major`** label to the PR to bump those instead. + +Publishing needs a repo secret **`PYPI_API_TOKEN`** (a PyPI API token; the +workflow uploads with `twine` as `__token__`). Add it under *Settings → Secrets +and variables → Actions*. Without it the version bump/tag still succeed and only +the publish step fails. diff --git a/pyproject.toml b/pyproject.toml index e0049a7..e5c7424 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "conference-analyzer" +name = "conflens" version = "0.1.13" description = "Browse, theme-classify, and topic-model conference papers with a NiceGUI UI." readme = "README.md" diff --git a/uv.lock b/uv.lock index c0afc01..5870e8c 100644 --- a/uv.lock +++ b/uv.lock @@ -296,7 +296,7 @@ wheels = [ ] [[package]] -name = "conference-analyzer" +name = "conflens" version = "0.1.13" source = { editable = "." } dependencies = [ @@ -342,7 +342,7 @@ name = "cuda-bindings" version = "13.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder" }, + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" }, @@ -371,34 +371,34 @@ wheels = [ [package.optional-dependencies] cudart = [ - { name = "nvidia-cuda-runtime" }, + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, ] cufft = [ - { name = "nvidia-cufft" }, + { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, ] cufile = [ - { name = "nvidia-cufile" }, + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, ] cupti = [ - { name = "nvidia-cuda-cupti" }, + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, ] curand = [ - { name = "nvidia-curand" }, + { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, ] cusolver = [ - { name = "nvidia-cusolver" }, + { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, ] cusparse = [ - { name = "nvidia-cusparse" }, + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc" }, + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, ] nvtx = [ - { name = "nvidia-nvtx" }, + { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -1277,7 +1277,7 @@ name = "nvidia-cublas" version = "13.1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cuda-nvrtc" }, + { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, @@ -1316,7 +1316,7 @@ name = "nvidia-cudnn-cu13" version = "9.20.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, @@ -1328,7 +1328,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, @@ -1358,9 +1358,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, - { name = "nvidia-cusparse" }, - { name = "nvidia-nvjitlink" }, + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, @@ -1372,7 +1372,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, From ab19d831d982a458e95cf817bf3b3067b8467b44 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 15:43:44 +0000 Subject: [PATCH 2/3] Load .env from the run directory; document env setup; PyPI badges - cli.py: load the .env from the current working directory via find_dotenv(usecwd=True). The old load_dotenv() searched from the installed package location, so a pip-installed `conflens` never picked up a .env next to where the user launched it (real env vars still work and take precedence). - tests/test_cli.py: .env-from-cwd is loaded, no-.env is a no-op, and a real env var overrides .env. - README: explain configuration (env var / .env in the run directory / in-app field, plus OpenReview creds) for the pip-installed CLI. - README badges: switch to PyPI stats (pypi version, pepy downloads, pypi pyversions) in place of the GitHub release/downloads/static-python badges. --- README.md | 22 +++++++++++++++++----- conflens/cli.py | 14 +++++++++++--- tests/test_cli.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 tests/test_cli.py diff --git a/README.md b/README.md index c06452d..3699224 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ [![Lint](https://img.shields.io/github/actions/workflow/status/picaultj/conflens/lint.yml?branch=main&logo=github&label=lint)](https://github.com/picaultj/conflens/actions/workflows/lint.yml) [![Tests](https://img.shields.io/github/actions/workflow/status/picaultj/conflens/test.yml?branch=main&logo=github&label=tests)](https://github.com/picaultj/conflens/actions/workflows/test.yml) -[![Downloads](https://img.shields.io/github/downloads/picaultj/conflens/total?logo=github&label=downloads)](https://github.com/picaultj/conflens/releases) -[![Release](https://img.shields.io/github/v/release/picaultj/conflens?logo=github&label=release&sort=semver)](https://github.com/picaultj/conflens/releases) +[![PyPI version](https://img.shields.io/pypi/v/conflens?logo=pypi&logoColor=white&label=pypi)](https://pypi.org/project/conflens/) +[![Downloads](https://pepy.tech/badge/conflens)](https://pepy.tech/project/conflens) +[![Python versions](https://img.shields.io/pypi/pyversions/conflens?logo=python&logoColor=white)](https://pypi.org/project/conflens/) [![Stars](https://img.shields.io/github/stars/picaultj/conflens?logo=github&style=flat)](https://github.com/picaultj/conflens/stargazers) [![Last commit](https://img.shields.io/github/last-commit/picaultj/conflens?logo=github)](https://github.com/picaultj/conflens/commits/main) [![Issues](https://img.shields.io/github/issues/picaultj/conflens?logo=github)](https://github.com/picaultj/conflens/issues) [![License: MPL 2.0](https://img.shields.io/badge/license-MPL%202.0-brightgreen.svg)](LICENSE) -[![Python 3.13+](https://img.shields.io/badge/python-3.13%2B-blue.svg?logo=python&logoColor=white)](https://www.python.org/) [![uv](https://img.shields.io/badge/managed%20by-uv-DE5FE9.svg?logo=uv&logoColor=white)](https://docs.astral.sh/uv/) [![Built with NiceGUI](https://img.shields.io/badge/UI-NiceGUI-2b6cb0.svg)](https://nicegui.io) @@ -70,8 +70,20 @@ uv run conflens # or: uv run python run.py Then open . -Keys are read from `.env` (loaded automatically) or the process environment; -you can also paste a key into the app's **API key** field at runtime. +**Configuration.** `conflens` needs an LLM provider key, supplied any of three +ways: + +- **Environment variable** — `export ANTHROPIC_API_KEY=…` (or `OPENAI_API_KEY`, + plus `OPENAI_BASE_URL` for an OpenAI-compatible endpoint; `LITELLM_API_KEY`). +- **A `.env` file** in the directory you launch `conflens` from — same keys, one + `NAME=value` per line. It's loaded automatically from the current working + directory (from a clone, copy [`.env.example`](.env.example) as a starting + point). Real environment variables take precedence over `.env`. +- **The in-app “API key” field** at runtime. + +OpenReview venues (ICLR / NeurIPS) additionally need `OPENREVIEW_TOKEN`, or +`OPENREVIEW_USERNAME` + `OPENREVIEW_PASSWORD` — set the same way, or typed into +the fields that appear in the UI when you pick the OpenReview source. `uv` provisions the right Python automatically (pinned to 3.13 via `.python-version`); you don't need to install it yourself. diff --git a/conflens/cli.py b/conflens/cli.py index 1d842e5..020ef53 100644 --- a/conflens/cli.py +++ b/conflens/cli.py @@ -16,12 +16,20 @@ def _index() -> None: def _load_env() -> None: - """Load variables from a local .env file, if present.""" + """Load a ``.env`` from the current working directory (or above), if present. + + ``find_dotenv(usecwd=True)`` searches from the directory the user runs + ``conflens`` in — not the installed package location — so a pip-installed CLI + picks up a ``.env`` sitting next to where it's launched. Real environment + variables always take precedence (``load_dotenv`` doesn't override them). + """ try: - from dotenv import load_dotenv + from dotenv import find_dotenv, load_dotenv except ImportError: # python-dotenv is a dependency, but stay defensive return - load_dotenv() + path = find_dotenv(usecwd=True) + if path: + load_dotenv(path) def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..a966292 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,34 @@ +import os + +from conflens.cli import _load_env + + +def test_load_env_reads_dotenv_from_run_directory(tmp_path, monkeypatch): + """A .env in the directory conflens is launched from is picked up. + + Guards the pip-installed case: `find_dotenv(usecwd=True)` must search the + working directory, not the installed package location. + """ + (tmp_path / ".env").write_text("CONFLENS_ENV_PROBE=hello\n") + monkeypatch.chdir(tmp_path) + monkeypatch.delenv("CONFLENS_ENV_PROBE", raising=False) + try: + _load_env() + assert os.environ.get("CONFLENS_ENV_PROBE") == "hello" + finally: + os.environ.pop("CONFLENS_ENV_PROBE", None) # load_dotenv sets os.environ directly + + +def test_load_env_no_dotenv_is_noop(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) # empty dir, no .env + monkeypatch.delenv("CONFLENS_ENV_PROBE", raising=False) + _load_env() # must not raise + assert os.environ.get("CONFLENS_ENV_PROBE") is None + + +def test_real_env_var_takes_precedence_over_dotenv(tmp_path, monkeypatch): + (tmp_path / ".env").write_text("CONFLENS_ENV_PROBE=from_dotenv\n") + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("CONFLENS_ENV_PROBE", "from_real_env") + _load_env() + assert os.environ.get("CONFLENS_ENV_PROBE") == "from_real_env" # not overridden From 14e1226130bf4f197b9c1cbb853436a0b7ca72a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Picault?= Date: Fri, 14 Aug 2026 17:57:35 +0200 Subject: [PATCH 3/3] Updated description --- pyproject.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e5c7424..c2460fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,17 @@ description = "Browse, theme-classify, and topic-model conference papers with a readme = "README.md" requires-python = ">=3.13" license = { text = "MPL-2.0" } +authors = [ + { name = "Jérôme Picault", email = "jerome.picault@rte-france.com" }, +] +maintainers = [ + { name = "Jérôme Picault", email = "jerome.picault@rte-france.com" }, +] +keywords = ["topics", "conferences", "LLM", "topic analysis", "report"] +classifiers = [ + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Development Status :: 4 - Beta", +] dependencies = [ "nicegui>=2.0", "anthropic>=0.40", @@ -23,6 +34,11 @@ bertopic = ["bertopic>=0.16"] conflens = "conflens.cli:main" conference-analyzer = "conflens.cli:main" # backward-compatible alias +[project.urls] +homepage = "https://github.com/picaultj/conflens" +repository = "https://github.com/picaultj/conflens" + + [dependency-groups] # Installed by default with `uv sync`; used for tests + linting / CI. dev = ["pytest>=8", "ruff>=0.6"]