Skip to content

Commit 5251c92

Browse files
committed
init
0 parents  commit 5251c92

16 files changed

+445
-0
lines changed

.github/workflows/ci.yml

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: main
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request: {}
8+
9+
concurrency:
10+
group: ${{ github.head_ref || github.run_id }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
lint:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v3
18+
- uses: actions/setup-python@v4
19+
with:
20+
python-version: "3.10"
21+
- uses: extractions/setup-just@v2
22+
- run: curl -LsSf https://astral.sh/uv/install.sh | sh
23+
- run: just install lint-ci
24+
25+
pytest:
26+
runs-on: ubuntu-latest
27+
strategy:
28+
fail-fast: false
29+
matrix:
30+
python-version:
31+
- "3.10"
32+
- "3.11"
33+
- "3.12"
34+
steps:
35+
- uses: actions/checkout@v3
36+
- uses: actions/setup-python@v4
37+
with:
38+
python-version: ${{ matrix.python-version }}
39+
- uses: extractions/setup-just@v2
40+
- run: curl -LsSf https://astral.sh/uv/install.sh | sh
41+
- run: just install test . --cov=. --cov-report xml
42+
- name: Upload coverage to Codecov
43+
uses: codecov/[email protected]
44+
env:
45+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
46+
with:
47+
files: ./coverage.xml
48+
flags: unittests
49+
name: codecov-${{ matrix.python-version }}

.github/workflows/publish.yml

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Publish Package
2+
3+
on:
4+
release:
5+
types:
6+
- published
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-python@v5
14+
with:
15+
python-version: "3.12"
16+
- uses: actions/cache@v4
17+
with:
18+
path: ~/.cache/uv
19+
key: publish-${{ hashFiles('pyproject.toml') }}
20+
- uses: extractions/setup-just@v2
21+
- run: curl -LsSf https://astral.sh/uv/install.sh | sh
22+
- run: just publish
23+
env:
24+
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}

.gitignore

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generic things
2+
*.pyc
3+
*~
4+
__pycache__/*
5+
*.swp
6+
*.sqlite3
7+
*.map
8+
.vscode
9+
.idea
10+
.DS_Store
11+
.env
12+
.mypy_cache
13+
.pytest_cache
14+
.ruff_cache
15+
.coverage
16+
htmlcov/
17+
coverage.xml
18+
pytest.xml
19+
dist/
20+
.python-version
21+
.venv
22+
uv.lock

Justfile

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
default: install lint test
2+
3+
install:
4+
uv lock --upgrade
5+
uv sync --all-extras --frozen
6+
7+
lint:
8+
uv run ruff format .
9+
uv run ruff check . --fix
10+
uv run mypy .
11+
12+
lint-ci:
13+
uv run ruff format . --check
14+
uv run ruff check . --no-fix
15+
uv run mypy .
16+
17+
test *args:
18+
uv run pytest {{ args }}
19+
20+
publish:
21+
rm -rf dist/*
22+
uv tool run --from build python -m build --installer uv
23+
uv tool run twine check dist/*
24+
uv tool run twine upload dist/* --username __token__ --password $PYPI_TOKEN

README.md

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Lite-Bootstrap package
2+
==
3+
[![Test Coverage](https://codecov.io/gh/modern-python/lite-bootstrap/branch/main/graph/badge.svg)](https://codecov.io/gh/modern-python/lite-bootstrap)
4+
[![MyPy Strict](https://img.shields.io/badge/mypy-strict-blue)](https://mypy.readthedocs.io/en/stable/getting_started.html#strict-mode-and-configuration)
5+
[![Supported versions](https://img.shields.io/pypi/pyversions/lite-bootstrap.svg)](https://pypi.python.org/pypi/lite-bootstrap)
6+
[![downloads](https://img.shields.io/pypi/dm/lite-bootstrap.svg)](https://pypistats.org/packages/lite-bootstrap)
7+
[![GitHub stars](https://img.shields.io/github/stars/modern-python/lite-bootstrap)](https://github.com/modern-python/lite-bootstrap/stargazers)
8+
9+
This package helps to build new microservices
10+
11+
## Quickstart:
12+
### Installation
13+
14+
```shell
15+
$ pip install lite-bootstrap
16+
```

lite_bootstrap/__init__.py

Whitespace-only changes.

lite_bootstrap/fastapi_bootstrap.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import dataclasses
2+
3+
import fastapi
4+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
5+
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
6+
7+
from lite_bootstrap.opentelemetry_bootstrap import OpenTelemetryBootstrap
8+
from lite_bootstrap.sentry_bootstrap import SentryBootstrap
9+
10+
11+
@dataclasses.dataclass(kw_only=True, slots=True)
12+
class FastAPIBootstrap:
13+
app: fastapi.FastAPI
14+
opentelemetry: OpenTelemetryBootstrap
15+
sentry: SentryBootstrap
16+
opentelemetry_excluded_urls: list[str] = dataclasses.field(default_factory=list)
17+
18+
def bootstrap_init(self) -> None:
19+
if self.sentry.sentry_dsn:
20+
self.sentry.start_tracing()
21+
self.app.add_middleware(SentryAsgiMiddleware)
22+
23+
self.opentelemetry.start_tracing()
24+
if self.opentelemetry.endpoint:
25+
FastAPIInstrumentor.instrument_app(
26+
app=self.app,
27+
tracer_provider=self.opentelemetry.tracer_provider,
28+
excluded_urls=",".join(self.opentelemetry_excluded_urls),
29+
)
30+
31+
def teardown(self) -> None:
32+
self.opentelemetry.teardown()
33+
if self.opentelemetry.endpoint:
34+
FastAPIInstrumentor.uninstrument_app(self.app)
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import dataclasses
2+
3+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
4+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined]
5+
from opentelemetry.sdk import resources
6+
from opentelemetry.sdk.trace import TracerProvider
7+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
8+
from opentelemetry.trace import set_tracer_provider
9+
10+
11+
@dataclasses.dataclass(kw_only=True, slots=True)
12+
class OpenTelemetryBootstrap:
13+
endpoint: str
14+
service_name: str
15+
instruments: list[BaseInstrumentor] = dataclasses.field(default_factory=list)
16+
tracer_provider: TracerProvider | None = dataclasses.field(init=False)
17+
18+
def start_tracing(self) -> None:
19+
if not self.endpoint:
20+
return
21+
22+
self.tracer_provider: TracerProvider = TracerProvider(
23+
resource=resources.Resource.create({resources.SERVICE_NAME: self.service_name}),
24+
)
25+
self.tracer_provider.add_span_processor(
26+
BatchSpanProcessor(
27+
OTLPSpanExporter(
28+
endpoint=self.endpoint,
29+
insecure=True,
30+
),
31+
),
32+
)
33+
34+
for instrument in self.instruments:
35+
instrument.instrument(
36+
tracer_provider=self.tracer_provider,
37+
)
38+
39+
set_tracer_provider(self.tracer_provider)
40+
41+
def teardown(self) -> None:
42+
if not self.endpoint:
43+
return
44+
45+
for instrument in self.instruments:
46+
instrument.uninstrument()

lite_bootstrap/py.typed

Whitespace-only changes.

lite_bootstrap/sentry_bootstrap.py

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import dataclasses
2+
import typing
3+
4+
import sentry_sdk
5+
6+
7+
@dataclasses.dataclass(kw_only=True, slots=True)
8+
class SentryBootstrap:
9+
sentry_dsn: str
10+
environment: str | None = None
11+
release: str | None = None
12+
max_breadcrumbs: int = 15
13+
attach_stacktrace: bool = True
14+
default_integrations: bool = True
15+
sentry_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
16+
tags: dict[str, str] | None = None
17+
18+
def start_tracing(self) -> None:
19+
if not self.sentry_dsn:
20+
return
21+
22+
sentry_sdk.init(
23+
dsn=self.sentry_dsn,
24+
environment=self.environment,
25+
max_breadcrumbs=self.max_breadcrumbs,
26+
attach_stacktrace=self.attach_stacktrace,
27+
default_integrations=self.default_integrations,
28+
release=self.release,
29+
**self.sentry_params,
30+
)
31+
tags: dict[str, str] = self.tags or {}
32+
sentry_sdk.set_tags(tags)

pyproject.toml

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
[project]
2+
name = "lite-bootstrap"
3+
description = "Lite package for bootstrapping new microservices"
4+
authors = [
5+
{ name = "Artur Shiriev", email = "[email protected]" },
6+
]
7+
readme = "README.md"
8+
requires-python = ">=3.10,<4"
9+
license = "MIT"
10+
keywords = [
11+
"python",
12+
"microservice",
13+
"bootstrap",
14+
"opentelemetry",
15+
"sentry",
16+
"error-tracing",
17+
"fastapi",
18+
]
19+
classifiers = [
20+
"Programming Language :: Python :: 3.10",
21+
"Programming Language :: Python :: 3.11",
22+
"Programming Language :: Python :: 3.12",
23+
"Typing :: Typed",
24+
"Topic :: Software Development :: Libraries",
25+
]
26+
dynamic = ["version"]
27+
packages = [
28+
{ include = "lite_bootstrap" },
29+
]
30+
31+
[project.urls]
32+
repository = "https://github.com/modern-python/lite-bootstrap"
33+
34+
[project.optional-dependencies]
35+
tracing = [
36+
"sentry-sdk",
37+
"opentelemetry-api",
38+
"opentelemetry-sdk",
39+
"opentelemetry-exporter-otlp",
40+
"opentelemetry-instrumentation",
41+
]
42+
fastapi = [
43+
"fastapi",
44+
"sentry-sdk",
45+
"opentelemetry-api",
46+
"opentelemetry-sdk",
47+
"opentelemetry-exporter-otlp",
48+
"opentelemetry-instrumentation",
49+
"opentelemetry-instrumentation-fastapi",
50+
]
51+
52+
[tool.uv]
53+
dev-dependencies = [
54+
"pytest",
55+
"pytest-cov",
56+
"httpx", # for test client
57+
"mypy",
58+
"ruff",
59+
]
60+
61+
[build-system]
62+
requires = ["hatchling", "hatch-vcs"]
63+
build-backend = "hatchling.build"
64+
65+
[tool.hatch.version]
66+
source = "vcs"
67+
68+
[tool.mypy]
69+
python_version = "3.10"
70+
strict = true
71+
72+
[tool.ruff]
73+
fix = true
74+
unsafe-fixes = true
75+
line-length = 120
76+
target-version = "py310"
77+
78+
[tool.ruff.format]
79+
docstring-code-format = true
80+
81+
[tool.ruff.lint]
82+
select = ["ALL"]
83+
ignore = [
84+
"D1", # allow missing docstrings
85+
"S101", # allow asserts
86+
"TCH", # ignore flake8-type-checking
87+
"FBT", # allow boolean args
88+
"ANN101", # missing-type-self
89+
"ANN102", # missing-type-cls
90+
"D203", # "one-blank-line-before-class" conflicting with D211
91+
"D213", # "multi-line-summary-second-line" conflicting with D212
92+
"COM812", # flake8-commas "Trailing comma missing"
93+
"ISC001", # flake8-implicit-str-concat
94+
]
95+
isort.lines-after-imports = 2
96+
isort.no-lines-before = ["standard-library", "local-folder"]
97+
98+
[tool.pytest.ini_options]
99+
addopts = "--cov=. --cov-report term-missing"
100+
101+
[tool.coverage.report]
102+
exclude_also = ["if typing.TYPE_CHECKING:"]

tests/__init__.py

Whitespace-only changes.

tests/conftest.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import typing
2+
3+
import pytest
4+
from fastapi import APIRouter, FastAPI
5+
from fastapi.responses import JSONResponse
6+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined]
7+
8+
9+
class CustomInstrumentor(BaseInstrumentor): # type: ignore[misc]
10+
def instrumentation_dependencies(self) -> typing.Collection[str]:
11+
return []
12+
13+
def _uninstrument(self, **kwargs: typing.Mapping[str, typing.Any]) -> None:
14+
pass
15+
16+
17+
@pytest.fixture
18+
def fastapi_app() -> FastAPI:
19+
app: typing.Final = FastAPI()
20+
router: typing.Final = APIRouter()
21+
22+
@router.get("/test")
23+
async def for_test_endpoint() -> JSONResponse:
24+
return JSONResponse(content={"key": "value"})
25+
26+
app.include_router(router)
27+
return app

0 commit comments

Comments
 (0)