Skip to content
This repository was archived by the owner on Aug 19, 2025. It is now read-only.

Commit 435c35e

Browse files
authored
Added GitHub action ci (#11)
* Added test deps to setup.py * Add scripts. * Add requirements.txt and setup.cfg * Fix lint errors. * Fix mypy errors. * Fix isort errors. * Add GitHub action to run tests.
1 parent 6f83395 commit 435c35e

17 files changed

Lines changed: 276 additions & 51 deletions

File tree

.github/workflows/test-suite.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
name: Test Suite
3+
4+
on:
5+
push:
6+
branches: ["master"]
7+
pull_request:
8+
branches: ["master"]
9+
10+
jobs:
11+
tests:
12+
name: "Python ${{ matrix.python-version }}"
13+
runs-on: "ubuntu-latest"
14+
15+
strategy:
16+
matrix:
17+
python-version: ["3.7", "3.8", "3.9"]
18+
19+
services:
20+
zookeeper:
21+
image: confluentinc/cp-zookeeper
22+
ports:
23+
- 32181:32181
24+
env:
25+
ZOOKEEPER_CLIENT_PORT: 32181
26+
ALLOW_ANONYMOUS_LOGIN: yes
27+
options: --hostname zookeeper
28+
kafka:
29+
image: confluentinc/cp-kafka
30+
ports:
31+
- 9092:9092
32+
- 29092:29092
33+
env:
34+
KAFKA_ZOOKEEPER_CONNECT: "zookeeper:32181"
35+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
36+
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT"
37+
KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT_HOST://localhost:29092,PLAINTEXT://localhost:9092"
38+
KAFKA_BROKER_ID: 1
39+
ALLOW_PLAINTEXT_LISTENER: yes
40+
options: --hostname kafka
41+
redis:
42+
image: redis:alpine
43+
ports:
44+
- 6379:6379
45+
postgres:
46+
image: postgres:12
47+
env:
48+
POSTGRES_DB: broadcaster
49+
POSTGRES_PASSWORD: postgres
50+
POSTGRES_HOST_AUTH_METHOD: trust
51+
POSTGRES_USER: postgres
52+
ports:
53+
- 5432:5432
54+
55+
steps:
56+
- uses: "actions/checkout@v2"
57+
- uses: "actions/setup-python@v2"
58+
with:
59+
python-version: "${{ matrix.python-version }}"
60+
- name: "Install dependencies"
61+
run: "scripts/install"
62+
- name: "Run linting checks"
63+
run: "scripts/check"
64+
- name: "Build package & docs"
65+
run: "scripts/build"
66+
- name: "Run tests"
67+
run: "scripts/test"
68+
- name: "Enforce coverage"
69+
run: "scripts/coverage"

broadcaster/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from ._base import Broadcast, Event
22

33
__version__ = "0.2.0"
4+
__all__ = ["Broadcast", "Event"]

broadcaster/_backends/base.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import typing
1+
from typing import Any
2+
3+
from .._base import Event
24

35

46
class BroadcastBackend:
5-
def __init__(self, url):
7+
def __init__(self, url: str) -> None:
68
raise NotImplementedError()
79

810
async def connect(self) -> None:
@@ -17,8 +19,8 @@ async def subscribe(self, group: str) -> None:
1719
async def unsubscribe(self, group: str) -> None:
1820
raise NotImplementedError()
1921

20-
async def publish(self, channel: str, message: typing.Any) -> None:
22+
async def publish(self, channel: str, message: Any) -> None:
2123
raise NotImplementedError()
2224

23-
async def next_published(self) -> typing.Tuple[str, typing.Any]:
25+
async def next_published(self) -> Event:
2426
raise NotImplementedError()

broadcaster/_backends/memory.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import asyncio
22
import typing
3-
from .base import BroadcastBackend
3+
44
from .._base import Event
5+
from .base import BroadcastBackend
56

67

78
class MemoryBackend(BroadcastBackend):

broadcaster/_backends/postgres.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import asyncio
2+
from typing import Any
3+
24
import asyncpg
3-
from .base import BroadcastBackend
5+
46
from .._base import Event
7+
from .base import BroadcastBackend
58

69

710
class PostgresBackend(BroadcastBackend):
@@ -24,7 +27,7 @@ async def unsubscribe(self, channel: str) -> None:
2427
async def publish(self, channel: str, message: str) -> None:
2528
await self._conn.execute("SELECT pg_notify($1, $2);", channel, message)
2629

27-
def _listener(self, *args) -> None:
30+
def _listener(self, *args: Any) -> None:
2831
connection, pid, channel, payload = args
2932
event = Event(channel=channel, message=payload)
3033
self._listen_queue.put_nowait(event)

broadcaster/_backends/redis.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import asyncio_redis
21
import typing
32
from urllib.parse import urlparse
4-
from .base import BroadcastBackend
3+
4+
import asyncio_redis
5+
56
from .._base import Event
7+
from .base import BroadcastBackend
68

79

810
class RedisBackend(BroadcastBackend):

broadcaster/_base.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import asyncio
2-
import typing
32
from contextlib import asynccontextmanager
3+
from typing import Any, AsyncGenerator, AsyncIterator, Dict, Optional
44
from urllib.parse import urlparse
55

66

77
class Event:
8-
def __init__(self, channel, message):
8+
def __init__(self, channel: str, message: str) -> None:
99
self.channel = channel
1010
self.message = message
1111

12-
def __eq__(self, other):
12+
def __eq__(self, other: object) -> bool:
1313
return (
1414
isinstance(other, Event)
1515
and self.channel == other.channel
1616
and self.message == other.message
1717
)
1818

19-
def __repr__(self):
20-
return f'Event(channel={self.channel!r}, message={self.message!r})'
19+
def __repr__(self) -> str:
20+
return f"Event(channel={self.channel!r}, message={self.message!r})"
2121

2222

2323
class Unsubscribed(Exception):
@@ -26,29 +26,36 @@ class Unsubscribed(Exception):
2626

2727
class Broadcast:
2828
def __init__(self, url: str):
29+
from broadcaster._backends.base import BroadcastBackend
30+
2931
parsed_url = urlparse(url)
30-
self._subscribers = {}
31-
if parsed_url.scheme == 'redis':
32-
from ._backends.redis import RedisBackend
32+
self._backend: BroadcastBackend
33+
self._subscribers: Dict[str, Any] = {}
34+
if parsed_url.scheme == "redis":
35+
from broadcaster._backends.redis import RedisBackend
36+
3337
self._backend = RedisBackend(url)
3438

35-
elif parsed_url.scheme in ('postgres', 'postgresql'):
36-
from ._backends.postgres import PostgresBackend
39+
elif parsed_url.scheme in ("postgres", "postgresql"):
40+
from broadcaster._backends.postgres import PostgresBackend
41+
3742
self._backend = PostgresBackend(url)
3843

39-
if parsed_url.scheme == 'kafka':
40-
from ._backends.kafka import KafkaBackend
44+
if parsed_url.scheme == "kafka":
45+
from broadcaster._backends.kafka import KafkaBackend
46+
4147
self._backend = KafkaBackend(url)
4248

43-
elif parsed_url.scheme == 'memory':
44-
from ._backends.memory import MemoryBackend
49+
elif parsed_url.scheme == "memory":
50+
from broadcaster._backends.memory import MemoryBackend
51+
4552
self._backend = MemoryBackend(url)
4653

47-
async def __aenter__(self) -> 'Broadcast':
54+
async def __aenter__(self) -> "Broadcast":
4855
await self.connect()
4956
return self
5057

51-
async def __aexit__(self, *args, **kwargs) -> None:
58+
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
5259
await self.disconnect()
5360

5461
async def connect(self) -> None:
@@ -68,11 +75,11 @@ async def _listener(self) -> None:
6875
for queue in list(self._subscribers.get(event.channel, [])):
6976
await queue.put(event)
7077

71-
async def publish(self, channel: str, message: typing.Any) -> None:
78+
async def publish(self, channel: str, message: Any) -> None:
7279
await self._backend.publish(channel, message)
7380

7481
@asynccontextmanager
75-
async def subscribe(self, channel: str) -> 'Subscriber':
82+
async def subscribe(self, channel: str) -> AsyncIterator["Subscriber"]:
7683
queue: asyncio.Queue = asyncio.Queue()
7784

7885
try:
@@ -93,10 +100,10 @@ async def subscribe(self, channel: str) -> 'Subscriber':
93100

94101

95102
class Subscriber:
96-
def __init__(self, queue):
103+
def __init__(self, queue: asyncio.Queue) -> None:
97104
self._queue = queue
98105

99-
async def __aiter__(self):
106+
async def __aiter__(self) -> Optional[AsyncGenerator]:
100107
try:
101108
while True:
102109
yield await self.get()

requirements.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
-e .[redis,postgres,kafka]
2+
3+
# Documentation
4+
mkdocs
5+
mkautodoc
6+
mkdocs-material
7+
8+
# Packaging
9+
twine
10+
wheel
11+
12+
# Tests & Linting
13+
autoflake
14+
black==20.8b1
15+
coverage==5.3
16+
flake8
17+
flake8-bugbear
18+
flake8-pie==0.5.*
19+
isort==5.*
20+
mypy
21+
pytest==5.*
22+
pytest-asyncio
23+
pytest-trio
24+
trio
25+
trio-typing

scripts/build

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/sh -e
2+
3+
if [ -d 'venv' ] ; then
4+
PREFIX="venv/bin/"
5+
else
6+
PREFIX=""
7+
fi
8+
9+
set -x
10+
11+
${PREFIX}python setup.py sdist bdist_wheel
12+
${PREFIX}twine check dist/*
13+
# ${PREFIX}mkdocs build

scripts/check

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/sh -e
2+
3+
export PREFIX=""
4+
if [ -d 'venv' ] ; then
5+
export PREFIX="venv/bin/"
6+
fi
7+
export SOURCE_FILES="broadcaster tests"
8+
9+
set -x
10+
11+
${PREFIX}black --check --diff --target-version=py37 $SOURCE_FILES
12+
${PREFIX}flake8 $SOURCE_FILES
13+
${PREFIX}mypy $SOURCE_FILES
14+
${PREFIX}isort --check --diff --project=httpx $SOURCE_FILES

0 commit comments

Comments
 (0)