Skip to content

Commit b2b9196

Browse files
howethomasclaude
andcommitted
Add GitHub Actions CI workflow and fix linting
- Add .github/workflows/tests.yml for automated testing - Run tests on Python 3.10, 3.11, 3.12 - Run Black formatting check - Run Ruff linting check - Upload coverage to Codecov - Format all code with Black - Fix all Ruff linting issues - Update pyproject.toml with refined Ruff configuration - Ignore N803 (Twilio uses PascalCase params) - Ignore B008 (FastAPI Depends pattern) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9129be2 commit b2b9196

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+958
-1059
lines changed

.github/workflows/tests.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
python-version: ["3.10", "3.11", "3.12"]
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Set up Python ${{ matrix.python-version }}
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
25+
- name: Install dependencies
26+
run: |
27+
python -m pip install --upgrade pip
28+
pip install git+https://github.com/vcon-dev/vcon-lib.git
29+
pip install -e ".[dev]"
30+
31+
- name: Run tests with coverage
32+
run: |
33+
pytest --cov=core --cov=adapters --cov-report=xml --cov-report=term-missing
34+
35+
- name: Upload coverage to Codecov
36+
uses: codecov/codecov-action@v4
37+
if: matrix.python-version == '3.12'
38+
with:
39+
files: ./coverage.xml
40+
fail_ci_if_error: false
41+
42+
lint:
43+
runs-on: ubuntu-latest
44+
steps:
45+
- uses: actions/checkout@v4
46+
47+
- name: Set up Python
48+
uses: actions/setup-python@v5
49+
with:
50+
python-version: "3.12"
51+
52+
- name: Install dependencies
53+
run: |
54+
python -m pip install --upgrade pip
55+
pip install ruff black
56+
57+
- name: Check formatting with Black
58+
run: black --check .
59+
60+
- name: Lint with Ruff
61+
run: ruff check .

adapters/__init__.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@
1515
"""
1616

1717
# Import all adapters for convenient access
18-
from . import twilio
19-
from . import freeswitch
20-
from . import asterisk
21-
from . import telnyx
22-
from . import bandwidth
18+
from . import asterisk, bandwidth, freeswitch, telnyx, twilio
2319

2420
__all__ = [
2521
"twilio",

adapters/asterisk/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Asterisk adapter for vCon telephony adapters."""
22

3-
from .config import AsteriskConfig
43
from .builder import AsteriskRecordingData, AsteriskVconBuilder
4+
from .config import AsteriskConfig
55
from .webhook import create_app
66

77
__all__ = [

adapters/asterisk/builder.py

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@
33
import logging
44
import os
55
from datetime import datetime, timezone
6-
from typing import Any, Dict, Optional
6+
from typing import Any
77

88
import requests
99
from requests.auth import HTTPBasicAuth
1010

1111
from core.base_builder import BaseRecordingData, BaseVconBuilder
1212

13-
1413
logger = logging.getLogger(__name__)
1514

1615

@@ -35,7 +34,7 @@ class AsteriskRecordingData(BaseRecordingData):
3534
- timestamp: Event timestamp
3635
"""
3736

38-
def __init__(self, event_data: Dict[str, Any]):
37+
def __init__(self, event_data: dict[str, Any]):
3938
"""Initialize from Asterisk event data.
4039
4140
Args:
@@ -47,25 +46,20 @@ def __init__(self, event_data: Dict[str, Any]):
4746
def recording_id(self) -> str:
4847
"""Asterisk recording name/ID."""
4948
return self._data.get(
50-
"recording_name",
51-
self._data.get("name", self._data.get("Uniqueid", ""))
49+
"recording_name", self._data.get("name", self._data.get("Uniqueid", ""))
5250
)
5351

5452
@property
5553
def from_number(self) -> str:
5654
"""Caller's phone number."""
57-
return self._data.get(
58-
"caller_id_num",
59-
self._data.get("CallerIDNum", "")
60-
)
55+
return self._data.get("caller_id_num", self._data.get("CallerIDNum", ""))
6156

6257
@property
6358
def to_number(self) -> str:
6459
"""Called phone number."""
6560
return self._data.get(
6661
"connected_line_num",
67-
self._data.get("ConnectedLineNum",
68-
self._data.get("extension", ""))
62+
self._data.get("ConnectedLineNum", self._data.get("extension", "")),
6963
)
7064

7165
@property
@@ -88,13 +82,10 @@ def direction(self) -> str:
8882
@property
8983
def recording_url(self) -> str:
9084
"""URL or path to the recording."""
91-
return self._data.get(
92-
"target_uri",
93-
self._data.get("recording_url", "")
94-
)
85+
return self._data.get("target_uri", self._data.get("recording_url", ""))
9586

9687
@property
97-
def recording_file_path(self) -> Optional[str]:
88+
def recording_file_path(self) -> str | None:
9889
"""Local file path to the recording."""
9990
target_uri = self._data.get("target_uri", "")
10091
if target_uri.startswith("file:"):
@@ -112,7 +103,7 @@ def recording_format(self) -> str:
112103
return self._data.get("format", "wav")
113104

114105
@property
115-
def duration_seconds(self) -> Optional[float]:
106+
def duration_seconds(self) -> float | None:
116107
"""Recording duration in seconds."""
117108
duration = self._data.get("duration")
118109
if duration is not None:
@@ -144,7 +135,7 @@ def start_time(self) -> datetime:
144135
return datetime.now(timezone.utc)
145136

146137
@property
147-
def platform_tags(self) -> Dict[str, str]:
138+
def platform_tags(self) -> dict[str, str]:
148139
"""Asterisk-specific metadata tags."""
149140
tags = {
150141
"asterisk_recording_name": self.recording_id,
@@ -183,9 +174,9 @@ def __init__(
183174
self,
184175
download_recordings: bool = True,
185176
recording_format: str = "wav",
186-
recordings_path: Optional[str] = None,
187-
ari_url: Optional[str] = None,
188-
ari_auth: Optional[tuple] = None,
177+
recordings_path: str | None = None,
178+
ari_url: str | None = None,
179+
ari_auth: tuple | None = None,
189180
):
190181
"""Initialize Asterisk vCon builder.
191182
@@ -201,10 +192,7 @@ def __init__(
201192
self.ari_url = ari_url
202193
self.ari_auth = ari_auth
203194

204-
def _download_recording(
205-
self,
206-
recording_data: BaseRecordingData
207-
) -> Optional[bytes]:
195+
def _download_recording(self, recording_data: BaseRecordingData) -> bytes | None:
208196
"""Download or read recording from Asterisk.
209197
210198
Args:
@@ -259,10 +247,8 @@ def _download_recording(
259247
logger.debug(f"Reading recording from file: {file_path}")
260248
with open(file_path, "rb") as f:
261249
return f.read()
262-
except IOError as e:
250+
except OSError as e:
263251
logger.error(f"Failed to read recording file: {e}")
264252

265-
logger.error(
266-
f"Could not access recording for {ast_data.recording_id}"
267-
)
253+
logger.error(f"Could not access recording for {ast_data.recording_id}")
268254
return None

adapters/asterisk/config.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Configuration management for Asterisk adapter."""
22

33
import os
4-
from typing import Optional
4+
55
from core.base_config import BaseConfig
66

77

@@ -12,7 +12,7 @@ class AsteriskConfig(BaseConfig):
1212
ARI (Asterisk REST Interface) connection and recording paths.
1313
"""
1414

15-
def __init__(self, env_file: Optional[str] = None):
15+
def __init__(self, env_file: str | None = None):
1616
"""Load configuration from environment.
1717
1818
Args:
@@ -21,10 +21,7 @@ def __init__(self, env_file: Optional[str] = None):
2121
super().__init__(env_file)
2222

2323
# State file specific to Asterisk
24-
self.state_file = os.getenv(
25-
"STATE_FILE",
26-
".asterisk_adapter_state.json"
27-
)
24+
self.state_file = os.getenv("STATE_FILE", ".asterisk_adapter_state.json")
2825

2926
# Asterisk ARI connection settings
3027
self.asterisk_host = os.getenv("ASTERISK_HOST", "localhost")
@@ -35,24 +32,23 @@ def __init__(self, env_file: Optional[str] = None):
3532
# ARI base URL
3633
ari_scheme = os.getenv("ASTERISK_ARI_SCHEME", "http")
3734
self.asterisk_ari_url = os.getenv(
38-
"ASTERISK_ARI_URL",
39-
f"{ari_scheme}://{self.asterisk_host}:{self.asterisk_ari_port}"
35+
"ASTERISK_ARI_URL", f"{ari_scheme}://{self.asterisk_host}:{self.asterisk_ari_port}"
4036
)
4137

4238
# Recording storage location (for local file access)
4339
self.recordings_path = os.getenv(
44-
"ASTERISK_RECORDINGS_PATH",
45-
"/var/spool/asterisk/recording"
40+
"ASTERISK_RECORDINGS_PATH", "/var/spool/asterisk/recording"
4641
)
4742

4843
# Webhook authentication (optional)
4944
self.webhook_secret = os.getenv("ASTERISK_WEBHOOK_SECRET")
5045

5146
# Whether to validate webhook signatures
52-
self.validate_webhook = os.getenv(
53-
"VALIDATE_ASTERISK_WEBHOOK",
54-
"false"
55-
).lower() in ("true", "1", "yes")
47+
self.validate_webhook = os.getenv("VALIDATE_ASTERISK_WEBHOOK", "false").lower() in (
48+
"true",
49+
"1",
50+
"yes",
51+
)
5652

5753
def get_ari_auth(self) -> tuple:
5854
"""Get ARI authentication tuple.

0 commit comments

Comments
 (0)