Skip to content

Commit

Permalink
Initial very rough release
Browse files Browse the repository at this point in the history
  • Loading branch information
symroe committed Mar 7, 2023
1 parent e31039d commit 18c0bea
Show file tree
Hide file tree
Showing 22 changed files with 1,870 additions and 0 deletions.
Empty file added __init__.py
Empty file.
16 changes: 16 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env python3

from setuptools import setup

setup(
name="dc_response_builder",
version="0.0.1",
description="Builds API responses",
author="Sym Roe",
author_email="sym.roe@democracyclub.org.uk",
setup_requires=["wheel"],
packages=["response_builder"],
package_dir={"response_builder": "."},
package_data={"response_builder": ["v1/*", "v1/**/*"]},
install_requires=["uk-election-ids==0.5.1", "pydantic[email]>=1.10,<2"],
)
Empty file added tests/__init__.py
Empty file.
32 changes: 32 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
# Make pytest fixtures from Factories
pydantic_factories's register_fixture decorator turns a factory into a pytest fixture,
however there's no nice way to import them into a project without importing the base class.
This makes it look, to editors and linters, like the classes are being unused, but the imports
are required to register the decorator.
We also can't use the factory directly.
Below we create sub-classes of all the factories we want and decorate them to turn them
into fixtures. Being in this file also means they're auto-loaded in tests.
"""

from pydantic_factories.plugins.pytest_plugin import register_fixture

from response_builder.v1.factories.councils import (
RegistrationFactory,
ElectoralServicesFactory,
)


@register_fixture(name="registration_factory")
class RegistrationFixture(RegistrationFactory):
...


@register_fixture(name="electoral_services_factory")
class ElectoralServicesFixture(ElectoralServicesFactory):
...
14 changes: 14 additions & 0 deletions tests/test_builders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from response_builder.v1.builders import RootBuilder, LocalBallotBuilder
from response_builder.v1.factories.ballots import LocalElectionBallotFactory
from response_builder.v1.factories.councils import NuneatonElectoralServices
from response_builder.v1.models.base import Ballot


def test_builder():
builder = RootBuilder()
ballot1 = LocalBallotBuilder()
ballot1.with_candidates(3)
builder.with_ballot(ballot1.build())
# builder.with_ballot(ballot2)
builder.with_electoral_services(NuneatonElectoralServices)
print(builder.build().json(indent=4))
12 changes: 12 additions & 0 deletions tests/test_factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from response_builder.v1.factories.councils import RegistrationFactory


def test_registration_factory_smoke_test():
factory = RegistrationFactory()
assert list(factory.build().dict().keys()) == [
"address",
"postcode",
"email",
"phone",
"website",
]
43 changes: 43 additions & 0 deletions tests/test_faker_providers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from unittest import mock

from faker import Faker

from response_builder.v1.factories.faker_providers import (
make_ward_name,
UKCouncilNamesProvider,
)


def test_make_ward_name():
with mock.patch("random.randrange", return_value=1):
# We should have two words
name = make_ward_name()
assert " " in name

with mock.patch("random.randrange", return_value=31):
# Slash in name
name = make_ward_name()
assert "/" in name

with mock.patch("random.randrange", return_value=36):
# Thing-with-other names
name = make_ward_name()
assert "-with-" in name

with mock.patch("random.randrange", return_value=21):
# Thing-with-other names
name = make_ward_name()
assert " & " in name


def test_faker_prodiver():
faker = Faker("en_GB")
faker.add_provider(UKCouncilNamesProvider)
# Not much we can test here as the return value is a random string,
# but at least calling it will act as a smoke test
assert type(faker.ward_name()) == str

organisation_name = faker.organisation_name()
assert organisation_name.lower() in faker.council_website()
assert organisation_name.lower() in faker.council_email()
assert organisation_name in faker.council_address()
143 changes: 143 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import json

from response_builder.v1.models.base import Date, RootModel
from response_builder.v1.models.councils import Registration, ElectoralServices


def test_electoral_services_eq_registration(
electoral_services_factory, registration_factory
):
kwargs = {
"council_id": "ABC",
"name": "Council",
"nation": "Council",
"address": "address",
"postcode": "SW1A 1AA",
"phone": "123456",
"email": "foo@bar.gov.uk",
"website": "https://example.com",
}

reg = Registration(**kwargs)
es = ElectoralServices(**kwargs)

assert es == reg
assert reg == es

other_reg = Registration(**kwargs)
other_reg.address = "NOT THE SAME"

assert es != other_reg
assert es != "foo"
assert reg != "foo"


def test_root_model():
model = RootModel()

model.dates = [Date(date="2021-05-04")]
# print(model.json())
# print(model.schema_json(indent=4))

data = """
{
"address_picker": false,
"addresses": [],
"dates": [
{
"date": "2018-05-03",
"polling_station": {
"polling_station_known": false,
"custom_finder": "http://www.eoni.org.uk/Offices/Postcode-Search-Results?postcode=BT28%202EY",
"report_problem_url": null,
"station": null
},
"advance_voting_station": null,
"notifications": [
{
"type": "voter_id",
"url": "http://www.eoni.org.uk/Vote/Voting-at-a-polling-place",
"title": "You need to show photographic ID to vote in this election",
"detail": "Voters in Northern Ireland are required to show one form of photo ID, like a passport or driving licence."
}
],
"ballots": [
{
"ballot_paper_id": "parl.west-tyrone.by.2018-05-03",
"ballot_title": "West Tyrone by-election",
"ballot_url": "https://developers.democracyclub.org.uk/api/v1/sandbox/elections/parl.west-tyrone.by.2018-05-03/",
"poll_open_date": "2018-11-22",
"elected_role": "Member of Parliament",
"metadata": {
"ni-voter-id": {
"url": "http://www.eoni.org.uk/Vote/Voting-at-a-polling-place",
"title": "You need to show photographic ID to vote in this election",
"detail": "Voters in Northern Ireland are required to show one form of photo ID, like a passport or driving licence."
}
},
"cancelled": false,
"replaced_by": null,
"replaces": null,
"election_id": "parl.2018-05-03",
"election_name": "UK Parliament elections",
"post_name": "West Tyrone",
"candidates_verified": false,
"voting_system": {
"slug": "FPTP",
"name": "First-past-the-post",
"uses_party_lists": false
},
"hustings": [
{
"title": "Local elections hustings",
"url": "https://example.com/hustings/",
"starts": "2018-04-28T18:00:00Z",
"ends": "2018-04-28T20:30:00Z",
"location": "Westminster Hall",
"postevent_url": "https://example.com/hustings/"
}
],
"seats_contested": 1,
"candidates": [],
"wcivf_url": "https://whocanivotefor.co.uk/elections/parl.2018-05-03/post-WMC:N06000018/west-tyrone"
}
]
}
],
"electoral_services": {
"council_id": "N09000007",
"name": "",
"nation": "Northern Ireland",
"email": "info@eoni.org.uk",
"phone": "",
"website": "http://www.eoni.org.uk/",
"postcode": "BT1 1ER",
"address": "The Electoral Office Headquarters St Anne's House 15 Church Street Belfast"
},
"registration": {
"council_id": "N09000007",
"name": "",
"nation": "Northern Ireland",
"email": "info@eoni.org.uk",
"phone": "",
"website": "http://www.eoni.org.uk/",
"postcode": "BT1 1ER",
"address": "The Electoral Office Headquarters St Anne's House 15 Church Street Belfast"
},
"postcode_location": {
"type": "Feature",
"properties": null,
"geometry": {
"type": "Point",
"coordinates": [
-6.206229,
54.550429
]
}
}
}
"""

assert model.validate(json.loads(data))
Empty file added v1/__init__.py
Empty file.
89 changes: 89 additions & 0 deletions v1/builders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from typing import Optional

from faker import Faker
from pydantic import BaseModel

from response_builder.v1.factories.ballots import LocalElectionBallotFactory
from response_builder.v1.factories.base import (
BaseModelFactory,
RootModelFactory,
)
from response_builder.v1.factories.candidates import CandidateFactory
from response_builder.v1.factories.councils import ElectoralServicesFactory
from response_builder.v1.models.base import Date, Ballot
from response_builder.v1.models.councils import ElectoralServices


class AbstractBuilder:
model: Optional[BaseModel] = None
factory: BaseModelFactory = None

def __init__(self, model=None, **kwargs):
self.kwargs = kwargs
self._factory = self.factory()

def build(self, **kwargs) -> BaseModel:
return self._factory.build(**kwargs)


class RootBuilder(AbstractBuilder):
factory = RootModelFactory

def __init__(self):
super().__init__()
self.faker = Faker()
self.factory.electoral_services = ElectoralServicesFactory().build()

def with_address_picker(self):
self.factory.address_picker = True
return self

def with_date(
self, date: Optional[str] = None, date_model: Optional[Date] = None
):
if all([date, date_model]):
raise ValueError("Either specify `date` or `date_model`, not both.")
if date:
date_model = Date(date=date)

self.factory.__model__.dates.append(date_model)
return self

def with_ballot(self, ballot_model: Ballot):
ballot_date = ballot_model.ballot_paper_id.split(".")[-1]
if ballot_date not in self.factory.__model__.dates:
self.with_date(ballot_date)
for date_model in self.factory.__model__.dates:
if date_model.date == ballot_date:
date_model.ballots.append(ballot_model)

def with_electoral_services(self, electoral_services: ElectoralServices):
self.factory.__model__.electoral_services = electoral_services
if not self.factory.__model__.registration:
self.factory.registration = electoral_services

def build(self):
return self.factory.__model__


class BallotBuilder(AbstractBuilder):
pass


class LocalBallotBuilder(BallotBuilder):
factory: Ballot = LocalElectionBallotFactory

def with_candidates(self, count, verified=False):
self.factory.candidates_verified = verified
self.factory.seats_contested = 1
for i in range(count):
self.with_candidate()
return self

def with_candidate(self, candidate=None, **kwargs):
if not candidate:
candidate = CandidateFactory().build(**kwargs)
if not hasattr(self.factory, "candidates"):
self.factory.candidates = []
self.factory.candidates.append(candidate)
return self
34 changes: 34 additions & 0 deletions v1/factories/ballots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from response_builder.v1.factories.base import (
BaseModelFactory,
MethodFactoryField,
FakerFactoryField,
LiteralFactoryField,
)
from response_builder.v1.factories.faker_providers import (
LocalBallotDataProvider,
)
from response_builder.v1.models.base import Ballot, VotingSystem


class BaseBallotFactory(BaseModelFactory[Ballot]):
"""
Can create a ballot, but most of the data in it will be random
"""

__model__ = Ballot
__fake_defaults__ = True
__faker_providers__ = [LocalBallotDataProvider]

metadata = LiteralFactoryField({})
ballot_url = FakerFactoryField("ballot_url")
wcivf_url = FakerFactoryField("wcivf_url")


class LocalElectionBallotFactory(BaseBallotFactory):
ballot_paper_id = FakerFactoryField("local_ballot_paper_id")
election_id = FakerFactoryField("local_election_id")
election_name = FakerFactoryField("local_election_name")
post_name = FakerFactoryField("ward_name")
elected_role = LiteralFactoryField("Local Councillor")
ballot_title = FakerFactoryField("local_ballot_title")
voting_system = VotingSystem()
Loading

0 comments on commit 18c0bea

Please sign in to comment.