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
6 changes: 5 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ jobs:
context: .
file: ./Dockerfile
push: false
cache-from: type=gha
# Load the image into the local Docker daemon so the scan uses this build
load: true
# Always rebuild from the latest base image so OS patches are not served from cache
pull: true
no-cache: true
cache-to: type=gha,mode=max
tags: ghcr.io/bigdata-com/bigdata-briefs:latest,ghcr.io/bigdata-com/bigdata-briefs:${{ github.event.release.tag_name }}
- name: Run security scan
Expand Down
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [4.0.0] - Unreleased
## [4.0.1] - 2026-10-19

### Security
- Upgraded all locked dependencies to their latest versions to address vulnerabilities reported in the Docker image scan (including `starlette`, `fastapi`, `urllib3`, `aiohttp`, `requests` and `certifi`).
- Rebuilt the Docker image as a multi-stage build on a pinned base image (`python:3.13-alpine3.24`) with the latest Alpine security patches applied. The runtime image no longer includes build tools, `git`, `curl`, `uv`, `pip` or dev dependencies, reducing its size from 569MB to 210MB.
- The release workflow now builds the image without cache and from the latest base image, and scans the freshly built image before pushing it.

### Fixed
- Declared `httpx` as a direct dependency; it was previously only available transitively through `openai`.
- Annotated SQL model datetime fields as `NaiveDatetime` to keep storing naive datetimes, as required by newer `sqlmodel` versions.
- Fixed entity tickers always being empty: the `ticker` field still used the `metadata_1` alias from the old SDK response, so tickers from the knowledge graph API were discarded and never included in the LLM prompts.
- Updated the `SentimentFilter` type to the `ranges` format already sent to the search API.
- Fixed type checking errors in the metrics classes.

### Changed
- Removed the UI section from the README, as the example frontend is temporarily disabled and `/` redirects to the API documentation at `/docs`.
- Pinned the `ruff` (0.15.22) and `ty` (0.0.89) versions used by the Makefile, so lint and type checks no longer change behaviour when new tool versions are released.

## [4.0.0] - 2025-11-21

### Added
- Added support for the new search API instead of the SDK for retrieval, allowing for more flexible queries and better performance.
Expand Down
75 changes: 44 additions & 31 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,51 +1,64 @@
FROM python:3.13-alpine
ARG PYTHON_IMAGE=python:3.13-alpine3.24

# ---- Build stage: install the project and its dependencies into a virtualenv ----
FROM ${PYTHON_IMAGE} AS builder

# Build tools are only needed to compile packages without prebuilt wheels
RUN apk add --no-cache build-base libffi-dev openssl-dev

COPY --from=ghcr.io/astral-sh/uv:0.13.1 /uv /bin/uv

# Use the image's Python so the virtualenv also works in the runtime stage
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_PYTHON_DOWNLOADS=0

WORKDIR /code

# Install dependencies first so this layer is cached when only the code changes
COPY pyproject.toml uv.lock README.md LICENSE /code/
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --no-install-project

COPY ./bigdata_briefs /code/bigdata_briefs
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --no-editable

# ---- Runtime stage: only the Python runtime and the virtualenv ----
FROM ${PYTHON_IMAGE}

ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1

# Install system dependencies for uv and Python packages
RUN apk update && apk add --no-cache \
curl \
ca-certificates \
build-base \
libffi-dev \
openssl-dev \
sqlite-dev \
gcc \
musl-dev \
git
PYTHONDONTWRITEBYTECODE=1 \
PATH="/code/.venv/bin:$PATH"

# Apply the latest Alpine security patches and remove pip, which is not
# needed at runtime and ships its own vendored packages
RUN apk upgrade --no-cache && \
python -m pip uninstall -y pip && \
rm -rf /root/.cache

# Create non-root user 'bigdata' and necessary directories
RUN adduser -D bigdata && \
mkdir /code /data && \
chown bigdata:bigdata /code /data
mkdir /data && \
chown bigdata:bigdata /data

# Remove setuid and setgid permissions for extra security
RUN find / -perm +6000 -type f -exec chmod a-s {} \; || true
RUN find / -xdev -perm +6000 -type f -exec chmod a-s {} \; || true

# Copy uv binary from external image before switching to non-root user
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
RUN chmod +x /bin/uv
COPY --from=builder --chown=bigdata:bigdata /code/.venv /code/.venv

# Switch to non-root user
USER bigdata
WORKDIR /code

# Copy project files
COPY pyproject.toml uv.lock README.md LICENSE /code/
COPY ./bigdata_briefs /code/bigdata_briefs

# Install project dependencies
RUN uv sync

# Expose service port
EXPOSE 8000

# Healthcheck to monitor container health
HEALTHCHECK --interval=30s --timeout=3s CMD curl http://localhost:8000/health || exit 1
# Healthcheck to monitor container health (uses Python, so curl is not needed)
HEALTHCHECK --interval=30s --timeout=3s CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=2)"]

# Set database connection string
ENV DB_STRING="sqlite:////data/bigdata_briefs.db"

# Launch application with uv
CMD ["uv", "run", "-m", "bigdata_briefs"]
# Launch application
CMD ["python", "-m", "bigdata_briefs"]
11 changes: 7 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
.PHONY: tests lint format

RUFF_VERSION := 0.15.22
TY_VERSION := 0.0.89

tests:
@uv run -m pytest --cov --cov-config=.coveragerc --cov-report term --cov-report xml:./coverage-reports/coverage.xml -s tests/*

lint:
@uvx ruff check --extend-select I --fix bigdata_briefs/ tests/
@uvx ruff@$(RUFF_VERSION) check --extend-select I --fix bigdata_briefs/ tests/

lint-check:
@uvx ruff check --extend-select I bigdata_briefs/ tests/
@uvx ruff@$(RUFF_VERSION) check --extend-select I bigdata_briefs/ tests/

format:
@uvx ruff format bigdata_briefs/ tests/
@uvx ruff@$(RUFF_VERSION) format bigdata_briefs/ tests/

type-check:
@uvx ty check bigdata_briefs/ tests/
@uvx ty@$(TY_VERSION) check bigdata_briefs/ tests/
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ docker run -d \
ghcr.io/bigdata-com/bigdata-briefs:latest
```

This will start the brief service locally on port 8000. You can then access the service @ `http://localhost:8000/` and the documentation for the API @ `http://localhost:8000/docs`.
This will start the brief service locally on port 8000. You can then access the documentation for the API @ `http://localhost:8000/docs`.

For a custom enterprise-ready solution, please contact us at [support@bigdata.com](mailto:support@bigdata.com)

Expand All @@ -61,12 +61,6 @@ We perform a pre-release security scan on our container images to detect vulnera

A brief is an executive summary of financially relevant information about a set of companies that form your watchlist.

### Using the UI
There is a very simple UI available @ `http://localhost:8000/`.

Set your watchlist ID, the relevant dates for your report and whether you want to filter the brief
only to novel information based on previously generated briefs and click on the "Generate Brief" button.

### Programmatically
You can generate a brief by sending a POST request to the `/briefs/create` endpoint with the required parameters.

Expand Down
6 changes: 4 additions & 2 deletions bigdata_briefs/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,13 @@ def health_check():

@app.get(
"/",
# summary="Example frontend for testing the thematic screener.",
# response_class=HTMLResponse,
# summary="Example frontend for testing the thematic screener.",
# response_class=HTMLResponse,
)
async def sample_frontend(_: str = Security(query_scheme)):
return RedirectResponse(url="/docs")


# # Get example values from the schema for all fields
# example_values = get_example_values_from_schema(BriefCreationRequest)
# return HTMLResponse(
Expand Down
4 changes: 2 additions & 2 deletions bigdata_briefs/api/sql_models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import uuid
from datetime import datetime

from pydantic import NaiveDatetime
from sqlalchemy.ext.mutable import MutableList
from sqlmodel import JSON, Column, Field, SQLModel


class SQLWorkflowStatus(SQLModel, table=True):
id: uuid.UUID = Field(primary_key=True)
last_updated: datetime
last_updated: NaiveDatetime
status: str
logs: list[str] = Field(
default_factory=list, sa_column=Column(MutableList.as_mutable(JSON))
Expand Down
11 changes: 7 additions & 4 deletions bigdata_briefs/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@


class Metrics(ABC):
metrics_queue: Queue
lock: Lock

@classmethod
@abstractmethod
def track_usage(cls, usage): ...
Expand Down Expand Up @@ -66,12 +69,12 @@ class WarningsMetrics(Metrics):
lock = Lock()

@classmethod
def track_usage(cls, warning_message: str):
def track_usage(cls, usage: str):
with cls.lock:
# Avoid logging duplicate warnings
if warning_message not in cls.warnings:
logger.info("A warning have been suppressed", warning=warning_message)
cls.warnings.add(warning_message)
if usage not in cls.warnings:
logger.info("A warning have been suppressed", warning=usage)
cls.warnings.add(usage)

@classmethod
def get_total_usage(cls) -> set[str]:
Expand Down
2 changes: 1 addition & 1 deletion bigdata_briefs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class Entity(BaseModel):
id: str
name: str
entity_type: str
ticker: Annotated[str | None, Field(default=None, validation_alias="metadata_1")]
ticker: str | None = None

_raw: Any = None # Field used to keep the original response from SDK

Expand Down
4 changes: 2 additions & 2 deletions bigdata_briefs/novelty/sql_models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import uuid
from datetime import datetime

from pydantic import NaiveDatetime
from sqlmodel import JSON, Column, Field, SQLModel


class SQLBulletPointEmbedding(SQLModel, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
entity_id: str
date: datetime
date: NaiveDatetime
embedding: list[float] = Field(sa_column=Column(JSON))
original_text: str
10 changes: 8 additions & 2 deletions bigdata_briefs/query_service/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ class EntityFilter(TypedDict):
any_of: List[str]


class SentimentFilter(TypedDict):
values: List[Literal["positive", "negative", "neutral"]]
class SentimentRange(TypedDict):
min: float
max: float


class SentimentFilter(TypedDict, total=False):
ranges: List[SentimentRange]
values: List[Literal["positive", "negative", "neutral"]] # Deprecated by the API


class SourceFilter(TypedDict):
Expand Down
7 changes: 4 additions & 3 deletions bigdata_briefs/sql_models.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import uuid
from datetime import datetime

from pydantic import NaiveDatetime
from sqlmodel import JSON, Column, Field, SQLModel


class SQLBriefReport(SQLModel, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
watchlist_id: str
created_at: datetime = Field(default_factory=datetime.now)
created_at: NaiveDatetime = Field(default_factory=datetime.now)
is_empty: bool = Field(default=False)
report_period_start: datetime
report_period_end: datetime
report_period_start: NaiveDatetime
report_period_end: NaiveDatetime
novelty_enabled: bool = Field(default=True)
brief_report: dict = Field(sa_column=Column(JSON))
2 changes: 1 addition & 1 deletion bigdata_briefs/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def get_report_with_sources(
# since some users might have older versions of the database with the report column stored as string
# Remove in future versions when a breaking change is acceptable
report.brief_report = json.loads(report.brief_report)
brief_report = BriefReport(**report.brief_report) # ty: ignore[missing-argument]
brief_report = BriefReport(**report.brief_report)
return brief_report
except Exception as e:
logger.error(f"Error reconstructing BriefReport from database records: {e}")
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "bigdata_briefs"
version = "4.0.0"
version = "4.0.1"
description = "Generate briefs based on finantially relevant information from Bigdata.com "
authors = [{name = "jaldana", email = "jaldana@ravenpack.com"}]
readme = "README.md"
Expand All @@ -20,6 +20,7 @@ dependencies = [
"uvicorn>=0.35.0",
"bigdata-client==2.19.0",
"sqlmodel>=0.0.24",
"httpx>=0.28.1",
]

[dependency-groups]
Expand Down
Loading
Loading