Reach Commons is a versatile utility library designed to streamline and enhance development workflows within the Reach ecosystem.
Project description
reach_commons
Reach's shared Python utility library — SMS encoding, phone validation, structured logging, persistence wrappers (Mongo, DynamoDB, S3, SQS, Firehose, KMS), Redis rate-limiting, and HTTP clients for internal Reach services (event-processor, callback-processor, reach-ops, reach-data-bridge), plus third-party integrations (HubSpot, Outscraper).
Distributed as the reach-commons wheel on
PyPI. Source of truth: https://gitlab.com/reach.ai/reach-commons.
Install
pip install reach-commons
Pin a version in requirements.txt / pyproject.toml like any other dep. Releases
are published from main by GitLab CI (see Release flow).
Quick example
from reach_commons.sms_smart_encoding import MessageSmartEncoding
msg = MessageSmartEncoding("Hello world!")
print(msg.encoding, msg.length, msg.segments) # GSM-7 12 1
Repo layout
reach_commons/
├── app_logging/ structured logger, http logger, deprecation tracker
├── clients/ thin HTTP wrappers for Reach internal services + 3rd-party APIs
│ ├── _auth.py shared Bearer-header helper (reads lambda_function_api_key)
│ ├── callback_processor.py
│ ├── event_processor.py
│ ├── hubspot.py
│ ├── nightly_lambda.py
│ ├── outscraper.py
│ ├── reach_data_bridge.py
│ └── reach_ops_api.py
├── mongo/ MongoDB customer persistence (sync + async)
├── reach_aws/ boto3 wrappers (dynamodb, s3, sqs, firehose, kms, rate limiter)
├── reach_base_model.py
├── redis_manager.py
├── sms_smart_encoding.py GSM-7 / UCS-2 detection + segment math
├── utils.py
└── validations.py phone-number validator (Twilio Lookups)
tests/ pytest, every suite uses @pytest.mark.parametrize
Required environment variables for consumers
Starting with version 0.18.56, secrets are no longer baked into the wheel.
Every credential is read at runtime from environment variables. Consumer lambdas
must inject these before instantiating any of the affected client classes;
unset values fall back to "" and produce loud 401s from upstream services
rather than silently working with stale defaults.
| Env var | Read by | Source of truth |
|---|---|---|
lambda_function_api_key |
_auth.reach_api_bearer_header() — used by EventProcessorClient, CallbackProcessorClient, ReachOpsApiClient, ReachDataBridgeClient |
SSM SecureString /api-keys/{env}/lambda-function-api-key (/terraform/shared-config.tf:19) |
TWILIO_ACCOUNT_SID or twilio_account_sid |
CommonsPhoneNumberValidation |
SSM /twilio/{env}/account_sid (/terraform/shared-config.tf:22) |
TWILIO_AUTH_TOKEN or twilio_auth_token |
CommonsPhoneNumberValidation |
SSM /twilio/{env}/auth_token (/terraform/shared-config.tf:23) |
outscraper_token |
OutscraperClient |
SSM /outscraper/{env}/token (/terraform/shared-config.tf:24) |
outscraper_webhook_url |
OutscraperClient |
SSM /outscraper/{env}/webhook_url (/terraform/shared-config.tf:25) |
ENV |
several clients to pick base_url ("Staging" / "Prod") |
usually set by Terraform Lambda config |
HUBSPOT_BASE_URL (optional) |
HubSpotClient constructor default |
env-injected |
Twilio is read with uppercase preferred, lowercase fallback — covers
reach-data-bridge-lambda / reach-ops-api-lambda (uppercase) and
sms-processor-lambda (lowercase), no consumer churn required.
Wiring: each consumer Lambda's serverless.yml must declare the env var and map
it to the SSM parameter published by /terraform/shared-config.tf (or to its own
per-lambda SSM resource). See POS-API-Puller-lambda/infra/env.tf for the modern
puller-style pattern; legacy lambdas use Serverless Framework variable resolvers.
Local development
poetry install # runtime + dev deps into .venv
poetry run pre-commit install # one-time: enables commit/push hooks
poetry run pytest # full suite
poetry run ruff check reach_commons tests
poetry run ruff format --check reach_commons tests
poetry run pyright reach_commons tests
Python floor: ^3.8. CI runs on python:3.13-slim.
Pre-commit hooks
.pre-commit-config.yaml installs three layers:
- On commit (fast): trailing-whitespace, end-of-file-fixer, check-yaml, check-toml, check-merge-conflict, check-added-large-files, ruff (lint + autofix), ruff-format.
- On push (slower): pyright with the runtime deps as
additional_dependencies.
Pyright runs only on push so day-to-day commits stay fast.
Release flow
GitLab CI publishes to PyPI automatically. Workflow:
- Open an MR against
main. - CI runs
lint+typecheck+teston every push (verify stage). - Merge to
main. - The
publish_pypijob compares theversioninpyproject.tomlagainst the latest version on PyPI:- If
pyproject.toml > PyPI: builds the wheel + sdist and uploads viapoetry publish. - Otherwise: logs
::: skip — local X <= PyPI Yand exits cleanly.
- If
So a release = bump version in pyproject.toml, merge to main. Done.
Pipeline shape
verify ──► lint (ruff check + ruff format --check)
──► typecheck (pyright)
──► test (pytest)
publish ──► publish_pypi (only on main, only when version bumped)
All jobs run on the self-hosted EC2 GitLab runner (tags: [ec2-instance]).
PyPI authentication
poetry publish reads the POETRY_PYPI_TOKEN_PYPI env var (Poetry's standard
convention). The token is injected at the gitlab-runner level, not as a
GitLab CI/CD variable — see /etc/gitlab-runner/config.toml on the runner host:
[[runners]]
name = "reach-ec2-runner"
environment = [
"POETRY_PYPI_TOKEN_PYPI=pypi-AgEIcHlwa…"
]
This keeps the token out of GitLab project settings entirely; rotation = SSH to
the runner, edit the line, systemctl restart gitlab-runner.
Branches
| Branch | Purpose |
|---|---|
main |
source of truth; merges here trigger PyPI publish when version bumped |
fix/*, feat/*, chore/* |
working branches; verify pipeline runs but no publish |
Architecture cheat-sheet
consumer Lambda
│
├─ os.environ[lambda_function_api_key] ──┐
│ ▼
│ reach_commons.clients._auth.reach_api_bearer_header()
│ │
├─ EventProcessorClient ────────────────┤
├─ CallbackProcessorClient ────────────────┤ "Authorization": "Bearer <token>"
├─ ReachOpsApiClient ────────────────┤
└─ ReachDataBridgeClient ────────────────┘
Four client classes share the same Bearer token via a single helper — drop the
env var on the Lambda and all four start sending Bearer (empty) → 401s.
Test layout
All test files in tests/ use @pytest.mark.parametrize. Adding a non-parametrized
def test_x is discouraged — collapse into parameters or split if behaviors truly
diverge.
| File | Coverage |
|---|---|
test_imports.py |
parametrized over every reach_commons.* module via pkgutil.walk_packages — auto-discovers new submodules |
test_sms_smart_encoding.py |
GSM-7 / UCS-2 detection, normalization map, segment math at length boundaries (160/161/306/307, 70/71) |
test_validations.py |
Twilio HTTP wrapper status-code → bool fork; env var precedence (uppercase / lowercase / unset) |
test_auth.py |
reach_api_bearer_header() with token set / empty / unset |
test_outscraper.py |
env var → constructor → empty fallback resolution; X-API-KEY header |
Migration history
- 2026-05-12 — Migrated from AWS CodeCommit (
reach-commons→ renamed toreach-commons-MOVED-TO-GITLAB, read-only archive) to GitLab. Replaced manualpython build.pypublish with GitLab CI publish-on-version-bump. Removed hardcoded secrets from the wheel (Twilio creds, Bearer tokens shared across four internal-API clients, Outscraper token + webhook URL); all secrets now read from env vars sourced from/terraform/shared-config.tfSSM. Adopted ruff (replacing black + isort) and pyright; added parametrized test suite. Version 0.18.55 → 0.18.56.
Service ownership
Owner: Reach Engineering. Issues / MR reviews via the GitLab repo. The legacy
CodeCommit archive at reach-commons-MOVED-TO-GITLAB is read-only and kept
only for git archaeology.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file reach_commons-0.18.56.tar.gz.
File metadata
- Download URL: reach_commons-0.18.56.tar.gz
- Upload date:
- Size: 34.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.8.5 CPython/3.13.13 Linux/6.14.0-1018-aws
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6d3444fcf2e7f779aafba5bee8cba5a1d74faf71f42faf94f9978035bc6ae384
|
|
| MD5 |
b16176a108880dadf58bd671413ed994
|
|
| BLAKE2b-256 |
5503fa33beffbc8ef8c4b200f826b66818c6cface6daf8d9614a835b41baf499
|
File details
Details for the file reach_commons-0.18.56-py3-none-any.whl.
File metadata
- Download URL: reach_commons-0.18.56-py3-none-any.whl
- Upload date:
- Size: 41.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.8.5 CPython/3.13.13 Linux/6.14.0-1018-aws
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
46cb2d4d5794b8772209a671b40aa04574a055be7a071107bef4fd407001b3a7
|
|
| MD5 |
57a31d5708ba694d2745d715c51dab37
|
|
| BLAKE2b-256 |
795a3f0aa0d8aab9a052d9d4d04808d12ef5762fe8272153b6dcb5012ba464f9
|