refactor: rename the application package to docsgpt

The backend import package is now docsgpt, the name it will carry on PyPI;
application was far too generic to install into anyone's site-packages.
git mv plus a mechanical rewrite of every import, dotted string and path
reference: 734 Python files, the compose files, Dockerfile, workflows, docs,
setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage
config, .gitignore. Behaviour is unchanged.

Kept for one release:
- A top-level application package whose meta-path finder resolves
  application.x.y to the already-imported docsgpt.x.y object, so old imports
  and entry points (celery -A application.app.celery,
  uvicorn application.asgi:asgi_app) keep working with a FutureWarning.
- Celery registers every application.* task name as an alias of its
  docsgpt.* task on start-up, so messages queued by the previous release still
  run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries
  the previous release wrote are left unread instead of firing twice.

The backend image builds from the repository root (docker build -f
docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore
allow-lists docsgpt/ and application/ and keeps caches, local data, .env
files, the sample index files and the Dockerfile out. Compose and the image
workflows point at the new context.
This commit is contained in:
Alex committed 2026-09-07 10:20:43 +01:00
1 parent 3e7f1f91b4
commit 574f96341e
984 files changed
+9847 -9682

No files matched your search

+25 -25
View File
@@ -1,9 +1,9 @@
"""Tests for application/mcp_server.py.
"""Tests for docsgpt/mcp_server.py.
The server module exposes one FastMCP tool, ``search_docs``, that reads
the caller's ``Authorization: Bearer <key>`` header via
``get_http_headers()`` and delegates to
``application.services.search_service.search``. These tests exercise
``docsgpt.services.search_service.search``. These tests exercise
the tool directly by patching ``get_http_headers`` and ``search``; the
full HTTP-layer plumbing (mount, lifespan, session handshake) is
covered by ``tests/test_asgi.py``.
@@ -18,20 +18,20 @@ import pytest
class TestSearchDocsTool:
@pytest.mark.asyncio
async def test_missing_bearer_raises_permission_error(self):
from application.mcp_server import search_docs
from docsgpt.mcp_server import search_docs
with patch(
"application.mcp_server.get_http_headers", return_value={}
"docsgpt.mcp_server.get_http_headers", return_value={}
):
with pytest.raises(PermissionError):
await search_docs(query="hi")
@pytest.mark.asyncio
async def test_non_bearer_header_raises_permission_error(self):
from application.mcp_server import search_docs
from docsgpt.mcp_server import search_docs
with patch(
"application.mcp_server.get_http_headers",
"docsgpt.mcp_server.get_http_headers",
return_value={"authorization": "Basic dXNlcjpwYXNz"},
):
with pytest.raises(PermissionError):
@@ -39,10 +39,10 @@ class TestSearchDocsTool:
@pytest.mark.asyncio
async def test_blank_bearer_token_raises_permission_error(self):
from application.mcp_server import search_docs
from docsgpt.mcp_server import search_docs
with patch(
"application.mcp_server.get_http_headers",
"docsgpt.mcp_server.get_http_headers",
return_value={"authorization": "Bearer "},
):
with pytest.raises(PermissionError):
@@ -50,16 +50,16 @@ class TestSearchDocsTool:
@pytest.mark.asyncio
async def test_invalid_api_key_raises_permission_error(self):
from application.mcp_server import search_docs
from application.services.search_service import InvalidAPIKey
from docsgpt.mcp_server import search_docs
from docsgpt.services.search_service import InvalidAPIKey
with (
patch(
"application.mcp_server.get_http_headers",
"docsgpt.mcp_server.get_http_headers",
return_value={"authorization": "Bearer bogus"},
),
patch(
"application.mcp_server.search", side_effect=InvalidAPIKey()
"docsgpt.mcp_server.search", side_effect=InvalidAPIKey()
),
):
with pytest.raises(PermissionError):
@@ -67,16 +67,16 @@ class TestSearchDocsTool:
@pytest.mark.asyncio
async def test_search_failed_bubbles_up(self):
from application.mcp_server import search_docs
from application.services.search_service import SearchFailed
from docsgpt.mcp_server import search_docs
from docsgpt.services.search_service import SearchFailed
with (
patch(
"application.mcp_server.get_http_headers",
"docsgpt.mcp_server.get_http_headers",
return_value={"authorization": "Bearer k"},
),
patch(
"application.mcp_server.search",
"docsgpt.mcp_server.search",
side_effect=SearchFailed("boom"),
),
):
@@ -85,16 +85,16 @@ class TestSearchDocsTool:
@pytest.mark.asyncio
async def test_happy_path_passes_args_and_returns_hits(self):
from application.mcp_server import search_docs
from docsgpt.mcp_server import search_docs
hits = [{"text": "t", "title": "T", "source": "s"}]
with (
patch(
"application.mcp_server.get_http_headers",
"docsgpt.mcp_server.get_http_headers",
return_value={"authorization": "Bearer the-key"},
),
patch(
"application.mcp_server.search", return_value=hits
"docsgpt.mcp_server.search", return_value=hits
) as mock_search,
):
out = await search_docs(query="q", chunks=7)
@@ -103,15 +103,15 @@ class TestSearchDocsTool:
@pytest.mark.asyncio
async def test_default_chunks_is_5(self):
from application.mcp_server import search_docs
from docsgpt.mcp_server import search_docs
with (
patch(
"application.mcp_server.get_http_headers",
"docsgpt.mcp_server.get_http_headers",
return_value={"authorization": "Bearer k"},
),
patch(
"application.mcp_server.search", return_value=[]
"docsgpt.mcp_server.search", return_value=[]
) as mock_search,
):
await search_docs(query="q")
@@ -119,15 +119,15 @@ class TestSearchDocsTool:
@pytest.mark.asyncio
async def test_bearer_scheme_case_insensitive(self):
from application.mcp_server import search_docs
from docsgpt.mcp_server import search_docs
with (
patch(
"application.mcp_server.get_http_headers",
"docsgpt.mcp_server.get_http_headers",
return_value={"authorization": "bearer lowercase-scheme"},
),
patch(
"application.mcp_server.search", return_value=[]
"docsgpt.mcp_server.search", return_value=[]
) as mock_search,
):
await search_docs(query="q")
+32 -32
View File
@@ -1,4 +1,4 @@
"""Unit tests for application/services/search_service.py.
"""Unit tests for docsgpt/services/search_service.py.
Tests exercise the service function in isolation — AgentsRepository is
stubbed via a patched ``db_readonly`` context manager, and
@@ -11,7 +11,7 @@ from unittest.mock import MagicMock, patch
import pytest
from application.services.search_service import (
from docsgpt.services.search_service import (
InvalidAPIKey,
SearchFailed,
_authorized_source_ids,
@@ -31,11 +31,11 @@ def _fake_db_readonly(agent_data):
yield MagicMock()
with patch(
"application.api.user.team_sharing.can_access", return_value=True
"docsgpt.api.user.team_sharing.can_access", return_value=True
), patch(
"application.services.search_service.db_readonly", _yield_conn
"docsgpt.services.search_service.db_readonly", _yield_conn
), patch(
"application.services.search_service.AgentsRepository",
"docsgpt.services.search_service.AgentsRepository",
return_value=agents_repo,
):
yield
@@ -75,9 +75,9 @@ class TestSearchInvalidAPIKey:
agents_repo.find_by_key.side_effect = RuntimeError("db down")
with patch(
"application.services.search_service.db_readonly", _yield_conn
"docsgpt.services.search_service.db_readonly", _yield_conn
), patch(
"application.services.search_service.AgentsRepository",
"docsgpt.services.search_service.AgentsRepository",
return_value=agents_repo,
):
with pytest.raises(SearchFailed):
@@ -93,12 +93,12 @@ class TestSearchEmptyWhenNoSources:
assert search("k", "q", 5) == []
def test_returns_empty_for_zero_chunks_without_db_lookup(self):
with patch("application.services.search_service.db_readonly") as mock_db:
with patch("docsgpt.services.search_service.db_readonly") as mock_db:
assert search("k", "q", 0) == []
mock_db.assert_not_called()
def test_returns_empty_for_negative_chunks_without_db_lookup(self):
with patch("application.services.search_service.db_readonly") as mock_db:
with patch("docsgpt.services.search_service.db_readonly") as mock_db:
assert search("k", "q", -1) == []
mock_db.assert_not_called()
@@ -115,7 +115,7 @@ class TestSearchResults:
}
]
with _fake_db_readonly(agent), patch(
"application.services.search_service.VectorCreator.create_vectorstore",
"docsgpt.services.search_service.VectorCreator.create_vectorstore",
return_value=fake_vs,
):
results = search("k", "q", 5)
@@ -133,7 +133,7 @@ class TestSearchResults:
fake_vs.search.return_value = [lc_doc]
with _fake_db_readonly(agent), patch(
"application.services.search_service.VectorCreator.create_vectorstore",
"docsgpt.services.search_service.VectorCreator.create_vectorstore",
return_value=fake_vs,
):
results = search("k", "q", 5)
@@ -151,7 +151,7 @@ class TestSearchResults:
fake_vs.search.return_value = docs
with _fake_db_readonly(agent), patch(
"application.services.search_service.VectorCreator.create_vectorstore",
"docsgpt.services.search_service.VectorCreator.create_vectorstore",
return_value=fake_vs,
):
results = search("k", "q", 3)
@@ -169,7 +169,7 @@ class TestSearchResults:
fake_vs.search.return_value = docs
with _fake_db_readonly(agent), patch(
"application.services.search_service.VectorCreator.create_vectorstore",
"docsgpt.services.search_service.VectorCreator.create_vectorstore",
return_value=fake_vs,
):
results = search("k", "q", 5)
@@ -194,7 +194,7 @@ class TestSearchResults:
return healthy_vs
with _fake_db_readonly(agent), patch(
"application.services.search_service.VectorCreator.create_vectorstore",
"docsgpt.services.search_service.VectorCreator.create_vectorstore",
side_effect=create_vs,
):
results = search("k", "q", 5)
@@ -208,7 +208,7 @@ class TestSearchResults:
{"text": "body", "metadata": {"filename": "document.pdf"}}
]
with _fake_db_readonly(agent), patch(
"application.services.search_service.VectorCreator.create_vectorstore",
"docsgpt.services.search_service.VectorCreator.create_vectorstore",
return_value=fake_vs,
):
results = search("k", "q", 5)
@@ -221,7 +221,7 @@ class TestSearchResults:
{"text": "Content without any title metadata at all", "metadata": {}}
]
with _fake_db_readonly(agent), patch(
"application.services.search_service.VectorCreator.create_vectorstore",
"docsgpt.services.search_service.VectorCreator.create_vectorstore",
return_value=fake_vs,
):
results = search("k", "q", 5)
@@ -232,7 +232,7 @@ class TestSearchResults:
# ``source_id=" "`` only — after strip() this leaves no real source.
agent = {"extra_source_ids": [" ", ""], "source_id": None}
with _fake_db_readonly(agent), patch(
"application.services.search_service.VectorCreator.create_vectorstore"
"docsgpt.services.search_service.VectorCreator.create_vectorstore"
) as mock_create:
results = search("k", "q", 5)
mock_create.assert_not_called()
@@ -254,14 +254,14 @@ class TestSourceAuthorization:
return {"id": "agent-1", "user_id": "owner", "extra_source_ids": [], **kw}
def test_readable_sources_pass_through(self, monkeypatch):
import application.api.user.team_sharing as ts
import docsgpt.api.user.team_sharing as ts
monkeypatch.setattr(ts, "can_access", lambda *a, **k: True)
agent = self._agent(extra_source_ids=["s1", "s2"])
assert _authorized_source_ids(None, agent, ["s1", "s2"]) == ["s1", "s2"]
def test_foreign_source_is_dropped(self, monkeypatch):
import application.api.user.team_sharing as ts
import docsgpt.api.user.team_sharing as ts
monkeypatch.setattr(ts, "can_access", lambda conn, k, sid, u: sid == "mine")
agent = self._agent()
@@ -269,7 +269,7 @@ class TestSourceAuthorization:
def test_team_shared_source_is_kept(self, monkeypatch):
"""A grant is legitimate access; only unreadable ids are dropped."""
import application.api.user.team_sharing as ts
import docsgpt.api.user.team_sharing as ts
monkeypatch.setattr(ts, "can_access", lambda *a, **k: True)
agent = self._agent(user_id="grantee")
@@ -280,7 +280,7 @@ class TestSourceAuthorization:
assert _authorized_source_ids(None, agent, ["s1"]) == []
def test_check_failure_fails_closed(self, monkeypatch):
import application.api.user.team_sharing as ts
import docsgpt.api.user.team_sharing as ts
def _boom(*a, **k):
raise RuntimeError("db down")
@@ -291,7 +291,7 @@ class TestSourceAuthorization:
def _serial_search_sources(query, source_ids, chunks):
"""The pre-fan-out ``_search_sources``, kept as a parity oracle."""
from application.services.search_service import VectorCreator, settings
from docsgpt.services.search_service import VectorCreator, settings
if chunks <= 0 or not source_ids:
return []
@@ -366,11 +366,11 @@ def _no_embedder_store(docs=None):
def _run_search_sources(query, source_ids, chunks, stores, serial=False):
"""Run ``_search_sources`` (or its serial oracle) against ``stores``."""
from application.services import search_service
from docsgpt.services import search_service
impl = _serial_search_sources if serial else search_service._search_sources
with patch(
"application.services.search_service.VectorCreator.create_vectorstore",
"docsgpt.services.search_service.VectorCreator.create_vectorstore",
side_effect=lambda _type, source_id, _key: stores[source_id],
):
return impl(query, source_ids, chunks)
@@ -555,10 +555,10 @@ class TestSearchSourcesFanOut:
raise RuntimeError("connection failed")
return stores[source_id]
from application.services import search_service
from docsgpt.services import search_service
with patch(
"application.services.search_service.VectorCreator.create_vectorstore",
"docsgpt.services.search_service.VectorCreator.create_vectorstore",
side_effect=_create,
):
results = search_service._search_sources("q", ["a", "b", "c"], 6)
@@ -596,10 +596,10 @@ class TestSearchSourcesFanOut:
embedder = _make_embedder()
stores = {"a": _make_store(embedder, [{"text": "hit a", "metadata": {}}])}
from application.services import search_service
from docsgpt.services import search_service
with patch(
"application.services.search_service.VectorCreator.create_vectorstore",
"docsgpt.services.search_service.VectorCreator.create_vectorstore",
side_effect=lambda _t, source_id, _k: stores[source_id],
):
results = search_service._search_sources("q", [" ", "a", ""], 6)
@@ -608,19 +608,19 @@ class TestSearchSourcesFanOut:
assert stores["a"].search.call_args.kwargs["k"] == 4
def test_all_blank_source_ids_build_no_store(self):
from application.services import search_service
from docsgpt.services import search_service
with patch(
"application.services.search_service.VectorCreator.create_vectorstore"
"docsgpt.services.search_service.VectorCreator.create_vectorstore"
) as mock_create:
assert search_service._search_sources("q", [" ", ""], 5) == []
mock_create.assert_not_called()
def test_zero_chunks_short_circuits(self):
from application.services import search_service
from docsgpt.services import search_service
with patch(
"application.services.search_service.VectorCreator.create_vectorstore"
"docsgpt.services.search_service.VectorCreator.create_vectorstore"
) as mock_create:
assert search_service._search_sources("q", ["a"], 0) == []
mock_create.assert_not_called()