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

+52 -52
View File
@@ -1,7 +1,7 @@
"""Tests for pure utility functions in application/worker.py.
"""Tests for pure utility functions in docsgpt/worker.py.
These cover helpers that don't require a Celery runtime (no task
instantiation). Aimed at maximizing coverage of ``application/worker.py``
instantiation). Aimed at maximizing coverage of ``docsgpt/worker.py``
without standing up Celery / redis.
"""
@@ -14,66 +14,66 @@ import pytest
class TestMetadataFromFilename:
def test_returns_title_dict(self):
from application.worker import metadata_from_filename
from docsgpt.worker import metadata_from_filename
assert metadata_from_filename("doc.txt") == {"title": "doc.txt"}
class TestNormalizeFileNameMap:
def test_empty_returns_empty_dict(self):
from application.worker import _normalize_file_name_map
from docsgpt.worker import _normalize_file_name_map
assert _normalize_file_name_map(None) == {}
assert _normalize_file_name_map("") == {}
assert _normalize_file_name_map({}) == {}
def test_json_string_parsed(self):
from application.worker import _normalize_file_name_map
from docsgpt.worker import _normalize_file_name_map
assert _normalize_file_name_map('{"a": "Alpha"}') == {"a": "Alpha"}
def test_invalid_json_returns_empty(self):
from application.worker import _normalize_file_name_map
from docsgpt.worker import _normalize_file_name_map
assert _normalize_file_name_map("not-json") == {}
def test_non_dict_returns_empty(self):
from application.worker import _normalize_file_name_map
from docsgpt.worker import _normalize_file_name_map
assert _normalize_file_name_map("[1, 2, 3]") == {}
def test_existing_dict_returned(self):
from application.worker import _normalize_file_name_map
from docsgpt.worker import _normalize_file_name_map
assert _normalize_file_name_map({"x": "y"}) == {"x": "y"}
class TestGetDisplayName:
def test_returns_none_for_empty_inputs(self):
from application.worker import _get_display_name
from docsgpt.worker import _get_display_name
assert _get_display_name({}, "a.txt") is None
assert _get_display_name({"a": "A"}, "") is None
assert _get_display_name(None, "a.txt") is None
def test_exact_rel_path_match(self):
from application.worker import _get_display_name
from docsgpt.worker import _get_display_name
assert _get_display_name({"sub/a.txt": "Alpha"}, "sub/a.txt") == "Alpha"
def test_basename_fallback(self):
from application.worker import _get_display_name
from docsgpt.worker import _get_display_name
assert _get_display_name({"a.txt": "Alpha"}, "sub/a.txt") == "Alpha"
def test_no_match_returns_none(self):
from application.worker import _get_display_name
from docsgpt.worker import _get_display_name
assert _get_display_name({"x.txt": "X"}, "sub/a.txt") is None
class TestApplyDisplayNames:
def test_non_dict_structure_returned_as_is(self):
from application.worker import _apply_display_names_to_structure
from docsgpt.worker import _apply_display_names_to_structure
assert _apply_display_names_to_structure("not a dict", {"a": "A"}) == "not a dict"
def test_empty_filemap_returned_as_is(self):
from application.worker import _apply_display_names_to_structure
from docsgpt.worker import _apply_display_names_to_structure
s = {"f.txt": {"type": "file", "size_bytes": 10}}
assert _apply_display_names_to_structure(s, {}) == s
def test_applies_display_name_to_files(self):
from application.worker import _apply_display_names_to_structure
from docsgpt.worker import _apply_display_names_to_structure
structure = {
"doc.txt": {"type": "file", "size_bytes": 10},
"sub": {
@@ -86,7 +86,7 @@ class TestApplyDisplayNames:
assert got["sub"]["nested.txt"]["display_name"] == "Nested"
def test_missing_display_name_leaves_untouched(self):
from application.worker import _apply_display_names_to_structure
from docsgpt.worker import _apply_display_names_to_structure
structure = {"x.txt": {"type": "file", "size_bytes": 10}}
got = _apply_display_names_to_structure(structure, {"y.txt": "Y"})
assert "display_name" not in got["x.txt"]
@@ -94,38 +94,38 @@ class TestApplyDisplayNames:
class TestGenerateRandomString:
def test_length(self):
from application.worker import generate_random_string
from docsgpt.worker import generate_random_string
assert len(generate_random_string(8)) == 8
assert len(generate_random_string(0)) == 0
def test_chars_are_letters(self):
from application.worker import generate_random_string
from docsgpt.worker import generate_random_string
s = generate_random_string(20)
assert all(c.isalpha() for c in s)
class TestIsPathSafe:
def test_allows_file_under_base(self, tmp_path):
from application.worker import _is_path_safe
from docsgpt.worker import _is_path_safe
base = str(tmp_path)
target = os.path.join(base, "sub", "file.txt")
assert _is_path_safe(base, target) is True
def test_rejects_path_above_base(self, tmp_path):
from application.worker import _is_path_safe
from docsgpt.worker import _is_path_safe
base = str(tmp_path)
target = "/tmp/outside.txt"
assert _is_path_safe(base, target) is False
def test_allows_base_itself(self, tmp_path):
from application.worker import _is_path_safe
from docsgpt.worker import _is_path_safe
base = str(tmp_path)
assert _is_path_safe(base, base) is True
class TestValidateZipSafety:
def test_rejects_nonexistent_zip(self, tmp_path):
from application.worker import (
from docsgpt.worker import (
_validate_zip_safety, ZipExtractionError,
)
# A path that doesn't exist → BadZipFile wrapped as ZipExtractionError
@@ -135,7 +135,7 @@ class TestValidateZipSafety:
)
def test_accepts_valid_small_zip(self, tmp_path):
from application.worker import _validate_zip_safety
from docsgpt.worker import _validate_zip_safety
zip_path = tmp_path / "ok.zip"
with zipfile.ZipFile(zip_path, "w") as zf:
zf.writestr("a.txt", "content")
@@ -143,7 +143,7 @@ class TestValidateZipSafety:
_validate_zip_safety(str(zip_path), str(tmp_path))
def test_rejects_path_traversal_in_zip(self, tmp_path):
from application.worker import (
from docsgpt.worker import (
_validate_zip_safety, ZipExtractionError,
)
zip_path = tmp_path / "bad.zip"
@@ -155,7 +155,7 @@ class TestValidateZipSafety:
_validate_zip_safety(str(zip_path), str(tmp_path))
def test_rejects_too_many_files(self, tmp_path):
from application.worker import (
from docsgpt.worker import (
_validate_zip_safety, ZipExtractionError,
)
zip_path = tmp_path / "many.zip"
@@ -164,14 +164,14 @@ class TestValidateZipSafety:
# patch the cap to trigger the "too many files" branch.
for i in range(20):
zf.writestr(f"f{i}.txt", "x")
with patch("application.worker.MAX_FILE_COUNT", 5):
with patch("docsgpt.worker.MAX_FILE_COUNT", 5):
with pytest.raises(ZipExtractionError):
_validate_zip_safety(str(zip_path), str(tmp_path))
class TestExtractZipRecursive:
def test_extracts_flat_zip(self, tmp_path):
from application.worker import extract_zip_recursive
from docsgpt.worker import extract_zip_recursive
zip_path = tmp_path / "in.zip"
with zipfile.ZipFile(zip_path, "w") as zf:
@@ -184,7 +184,7 @@ class TestExtractZipRecursive:
assert (extract_to / "b.txt").exists()
def test_extracts_nested_zip_recursively(self, tmp_path):
from application.worker import extract_zip_recursive
from docsgpt.worker import extract_zip_recursive
# Create inner.zip
inner = tmp_path / "inner.zip"
@@ -204,7 +204,7 @@ class TestExtractZipRecursive:
assert found, "expected nested zip to be extracted"
def test_nested_archives_share_one_expansion_budget(self, tmp_path):
from application.worker import extract_zip_recursive, ZipExtractionError
from docsgpt.worker import extract_zip_recursive, ZipExtractionError
inner = tmp_path / "inner.zip"
with zipfile.ZipFile(inner, "w", zipfile.ZIP_STORED) as zf:
@@ -219,8 +219,8 @@ class TestExtractZipRecursive:
limit = inner.stat().st_size + 30
extract_to = tmp_path / "out"
extract_to.mkdir()
with patch("application.worker.MAX_UNCOMPRESSED_SIZE", limit), patch(
"application.worker.MAX_COMPRESSION_RATIO", 10_000,
with patch("docsgpt.worker.MAX_UNCOMPRESSED_SIZE", limit), patch(
"docsgpt.worker.MAX_COMPRESSION_RATIO", 10_000,
):
with pytest.raises(ZipExtractionError):
extract_zip_recursive(str(outer), str(extract_to))
@@ -230,7 +230,7 @@ class TestExtractZipRecursive:
class TestDownloadFile:
def test_writes_file_on_success(self, tmp_path):
from application.worker import download_file
from docsgpt.worker import download_file
dest = tmp_path / "downloaded.bin"
mock_response = MagicMock()
@@ -238,18 +238,18 @@ class TestDownloadFile:
mock_response.raise_for_status = MagicMock()
with patch(
"application.worker.requests.get", return_value=mock_response,
"docsgpt.worker.requests.get", return_value=mock_response,
):
download_file("http://ex/foo", {}, str(dest))
assert dest.read_bytes() == b"file-content"
def test_raises_on_request_error(self, tmp_path):
from application.worker import download_file
from docsgpt.worker import download_file
import requests
with patch(
"application.worker.requests.get",
"docsgpt.worker.requests.get",
side_effect=requests.RequestException("boom"),
):
with pytest.raises(requests.RequestException):
@@ -260,19 +260,19 @@ class TestDownloadFile:
class TestUploadIndex:
def test_non_faiss_posts_data_only(self, tmp_path):
from application.worker import upload_index
from docsgpt.worker import upload_index
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
with patch(
"application.worker.settings.VECTOR_STORE", "milvus"
"docsgpt.worker.settings.VECTOR_STORE", "milvus"
), patch(
"application.worker.settings.API_URL", "http://api/"
"docsgpt.worker.settings.API_URL", "http://api/"
), patch(
"application.worker.settings.INTERNAL_KEY", "k"
"docsgpt.worker.settings.INTERNAL_KEY", "k"
), patch(
"application.worker.requests.post", return_value=mock_response,
"docsgpt.worker.requests.post", return_value=mock_response,
) as mock_post:
upload_index(str(tmp_path), {"source_id": "1"})
@@ -282,29 +282,29 @@ class TestUploadIndex:
assert kwargs["data"] == {"source_id": "1"}
def test_faiss_missing_file_raises(self, tmp_path):
from application.worker import upload_index
from docsgpt.worker import upload_index
with patch(
"application.worker.settings.VECTOR_STORE", "faiss"
"docsgpt.worker.settings.VECTOR_STORE", "faiss"
):
with pytest.raises(FileNotFoundError):
upload_index(str(tmp_path), {"source_id": "1"})
def test_faiss_uploads_both_files(self, tmp_path):
from application.worker import upload_index
from docsgpt.worker import upload_index
(tmp_path / "index.faiss").write_bytes(b"faiss-bytes")
(tmp_path / "index.pkl").write_bytes(b"pkl-bytes")
mock_response = MagicMock()
with patch(
"application.worker.settings.VECTOR_STORE", "faiss"
"docsgpt.worker.settings.VECTOR_STORE", "faiss"
), patch(
"application.worker.settings.API_URL", "http://api/"
"docsgpt.worker.settings.API_URL", "http://api/"
), patch(
"application.worker.settings.INTERNAL_KEY", ""
"docsgpt.worker.settings.INTERNAL_KEY", ""
), patch(
"application.worker.requests.post", return_value=mock_response,
"docsgpt.worker.requests.post", return_value=mock_response,
) as mock_post:
upload_index(str(tmp_path), {"source_id": "1"})
@@ -322,7 +322,7 @@ class TestCountStructureFiles:
1,474-file repo logged "44 files"."""
def test_counts_nested_leaves_not_top_level_keys(self):
from application.worker import count_structure_files
from docsgpt.worker import count_structure_files
structure = {
"README.md": {"type": "text/markdown", "size_bytes": 10, "token_count": 3},
@@ -338,7 +338,7 @@ class TestCountStructureFiles:
assert count_structure_files(structure) == 4
def test_empty_and_non_dict_are_zero(self):
from application.worker import count_structure_files
from docsgpt.worker import count_structure_files
assert count_structure_files({}) == 0
assert count_structure_files(None) == 0
@@ -351,7 +351,7 @@ class TestDirectoryStructureAccumulatesChunks:
size and token count."""
def test_multi_chunk_file_sums_its_chunks(self):
from application.worker import add_file_to_structure, count_structure_files
from docsgpt.worker import add_file_to_structure, count_structure_files
structure: dict = {}
for tokens, size in [(900, 3600), (900, 3600), (120, 480)]:
@@ -367,7 +367,7 @@ class TestDirectoryStructureAccumulatesChunks:
assert count_structure_files(structure) == 1
def test_distinct_files_stay_separate(self):
from application.worker import add_file_to_structure, count_structure_files
from docsgpt.worker import add_file_to_structure, count_structure_files
structure: dict = {}
for path in ["a.md", "src/b.py", "src/deep/c.py"]:
@@ -380,7 +380,7 @@ class TestDirectoryStructureAccumulatesChunks:
assert structure["a.md"]["token_count"] == 4
def test_empty_path_is_ignored(self):
from application.worker import add_file_to_structure
from docsgpt.worker import add_file_to_structure
structure: dict = {}
add_file_to_structure(