-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
22 changed files
with
1,870 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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): | ||
... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Oops, something went wrong.