Skip to content
This repository was archived by the owner on Jun 3, 2023. It is now read-only.

Commit 9feb0f9

Browse files
committed
Resolve circular import and other improvements
* Build app from factory * Add development dependencies * Improve static type annotations for dns logic * Remove explicit class references * Make the views object a blueprint * Indirect app reference via flask.current_app * Use built-in logger * Fix import in runner * Gitignore tags, .env * Add tags, outdated as make targets
1 parent 63570fc commit 9feb0f9

File tree

8 files changed

+73
-38
lines changed

8 files changed

+73
-38
lines changed

‎.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
*.pdf
33
__pycache__/
44
.mypy_cache/
5-
aws_credentials.sh
65
venv/
6+
.env
7+
tags

‎Makefile

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1-
venv: requirements.txt
1+
VIRTUAL_ENV ?= venv
2+
SRC = app/
3+
4+
venv: requirements.txt requirements_dev.txt
25
@python3 -m venv $@
3-
@source $@/bin/activate && pip install -r $<
6+
@source $@/bin/activate && pip install -r $< -r requirements_dev.txt
7+
8+
tags: $(SRC)
9+
@ctags --languages=python --python-kinds=-i -R $(SRC)
10+
11+
.PHONY: outdated
12+
outdated:
13+
@(source $(VIRTUAL_ENV)/bin/activate && pip list --outdated)
414

515
.PHONY: lint
616
lint:
7-
@pylint -f colorized app/
17+
@pylint -f colorized $(SRC)
818

919
.PHONY: typecheck
1020
typecheck:
11-
@mypy app/
21+
@mypy $(SRC)

‎Procfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
web: gunicorn app:app --preload --log-file=-
1+
web: gunicorn "app:create_app()" --preload --log-file=-

‎app/__init__.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
"""App"""
2+
3+
import os
4+
25
from flask import Flask
36

4-
app = Flask(__name__)
5-
from app import views
7+
def create_app() -> Flask:
8+
"""app factory"""
9+
app = Flask(__name__)
10+
app.secret_key = os.environ["SECRET_KEY"]
11+
12+
with app.app_context():
13+
# We import here because views uses flask.current_app to avoid a
14+
# circular import
15+
from .views import BP as views_bp
16+
app.register_blueprint(views_bp)
17+
18+
return app

‎app/dns/dns.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""DNS logic"""
33

44
import os
5+
from typing import List
56

67
import boto
78
import route53 # type: ignore
@@ -12,13 +13,13 @@
1213
# pylint: disable=too-few-public-methods
1314
class Cluster():
1415
"""A class for cluster objects"""
15-
instances: list = []
16-
records: list = []
17-
zone = None
16+
instances: List["Cluster"] = []
17+
records: List["route53.resource_record_set.AResourceRecordSet"] = []
18+
zone: "route53.hosted_zone.HostedZone" = None
1819
n = 4 # Number of unique clusters
1920

2021
def __init__(self, cluster_id: int) -> None:
21-
self.__class__.instances.append(self)
22+
self.instances.append(self)
2223
self.cluster_id = cluster_id
2324
self.cluster_name = self._name()
2425
self.subdomain = self._subdomain()
@@ -47,21 +48,21 @@ class Server(Cluster):
4748

4849
def __init__(self, cluster_id: int, friendly_name: str) -> None:
4950
super().__init__(cluster_id)
50-
self.server_id = len(self.__class__.instances)
51+
self.server_id = len(self.instances)
5152
self.friendly_name = friendly_name
5253
self.ip_string = "0.0.0.0"
5354
self.dns = "NONE"
5455

5556
def add_to_rotation(self) -> None:
5657
"""Adds the server's IP to the cluster's subdomain."""
57-
fqdn = self.subdomain + "." + self.__class__.zone.name
58+
fqdn = self.subdomain + "." + self.zone.name
5859

5960
# If records could simply be added & removed, this would make the
6061
# UI behave better with synchronous post requests.
6162
# Instead we have to rewrite the whole record.
6263

6364
ips: list = []
64-
for record in self.__class__.records:
65+
for record in self.records:
6566
if fqdn == record.name:
6667
ips = record.records[:]
6768
break
@@ -72,7 +73,7 @@ def add_to_rotation(self) -> None:
7273
aws_secret_access_key=AWS_ACCESS_SECRET)
7374

7475
changes = boto.route53.record.ResourceRecordSets(conn, # type: ignore
75-
self.__class__.zone.id)
76+
self.zone.id)
7677
change = changes.add_change("UPSERT", fqdn, "A")
7778

7879
for ip_address in set(ips):
@@ -82,10 +83,10 @@ def add_to_rotation(self) -> None:
8283

8384
def remove_from_rotation(self) -> None:
8485
"""Removes the server's IP from the DNS record."""
85-
fqdn = self.subdomain + "." + self.__class__.zone.name
86+
fqdn = self.subdomain + "." + self.zone.name
8687

8788
ips: list = []
88-
for record in self.__class__.records:
89+
for record in self.records:
8990
if fqdn == record.name:
9091
ips = record.records[:]
9192
break
@@ -99,7 +100,7 @@ def remove_from_rotation(self) -> None:
99100
aws_secret_access_key=AWS_ACCESS_SECRET)
100101

101102
changes = boto.route53.record.ResourceRecordSets(conn, # type: ignore
102-
self.__class__.zone.id)
103+
self.zone.id)
103104

104105
# This should be simple, but seemingly the API complains unless
105106
# it's updated in this arduous fashion.
@@ -124,8 +125,9 @@ def dns() -> list:
124125

125126
zone = list(conn.list_hosted_zones())[0]
126127
records = [record for record in zone.record_sets]
127-
records = list(filter(lambda x: x.rrset_type == "A", records))
128-
records = list(filter(lambda x: x.name != zone.name, records))
128+
129+
records = [r for r in zone.record_sets \
130+
if (r.rrset_type == "A" and r.name != zone.name)]
129131

130132
Cluster.zone = zone
131133
Cluster.records = records
@@ -172,7 +174,7 @@ def print_servers() -> None:
172174

173175
def print_dns(dns_records: list) -> None:
174176
"""ASCII analogy of the DNS UI."""
175-
print("\n\033[1mDomain\t\t\t IP(s)\t\t Server(s)\t Cluster\033[0m")
177+
print("\n\033[1mDomain\t\t\t\t\t IP(s)\t\t Server(s)\t Cluster\033[0m")
176178
for record in dns_records:
177179
matching_servers = []
178180
for server in Server.instances:

‎app/views.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,43 @@
1-
"""flask views object"""
1+
"""Blueprint for views"""
22

33
import flask
44
import werkzeug
5-
from app import app
65
from .dns.dns import Cluster, Server, dns, update_server_dns, create_instances
76

8-
app.secret_key = "lQq8UJPnzpqWpKAIE0PrC0tddlWGXXuBVHwbtks65j0="
7+
BP = flask.Blueprint("views", __name__, url_prefix="/")
98

10-
@app.route("/")
11-
@app.route("/index")
9+
APP = flask.current_app
10+
11+
@APP.route("/")
12+
@APP.route("/index")
1213
def index() -> flask.signals.template_rendered:
1314
"""index"""
1415
links = [
1516
{
16-
'title': 'Currently published DNS entries',
17-
'url': 'dns'
17+
"title": "Currently published DNS entries",
18+
"url": "dns"
1819
},
1920
{
20-
'title': 'Servers',
21-
'url': 'servers'
21+
"title": "Servers",
22+
"url": "servers"
2223
}
2324
]
2425
return flask.render_template("index.html", title="Home", links=links)
2526

26-
@app.route("/servers")
27+
@APP.route("/servers")
2728
def servers_ui() -> flask.signals.template_rendered:
2829
"""renders the server UI"""
2930
update_server_dns(dns())
3031
servers = Server.instances
3132
return flask.render_template("servers.html", title="Servers", servers=servers)
3233

33-
@app.route("/dns")
34+
@APP.route("/dns")
3435
def dns_ui() -> flask.signals.template_rendered:
3536
"""renders the DNS UI"""
3637
return flask.render_template("dns.html", title="DNS", zone=Cluster.zone,
3738
servers=Server.instances, dns_records=dns())
3839

39-
@app.route("/rotate", methods=["GET", "POST"])
40+
@APP.route("/rotate", methods=["GET", "POST"])
4041
def rotate() -> werkzeug.wrappers.Response:
4142
""" Moves the server(s) into / out of DNS rotation """
4243
if flask.request.method == "POST":
@@ -49,13 +50,13 @@ def rotate() -> werkzeug.wrappers.Response:
4950
flask.flash("Added server " + server.friendly_name + \
5051
" to DNS A record " + server.subdomain + \
5152
"." + server.zone.name)
52-
print("add", server.friendly_name)
53+
APP.logger.info("add %s", server.friendly_name)
5354
server.add_to_rotation()
5455
elif action == "remove":
5556
flask.flash("Removed server " + server.friendly_name + \
5657
" from DNS A record " + server.subdomain + \
5758
"." + server.zone.name)
58-
print("remove", server.friendly_name)
59+
APP.logger.info("remove %s", server.friendly_name)
5960
server.remove_from_rotation()
6061
break
6162

‎requirements_dev.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mypy >= 0.670
2+
pylint >= 2.3.1
3+
python-dotenv >= 0.10.1

‎run.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
#!/usr/bin/env python3
22
"""dns_automator runner"""
33

4-
from app import app
4+
from app import create_app
55

6-
if __name__ == "__main__":
6+
def main() -> None:
7+
"""Entry point"""
8+
app = create_app()
79
app.run(debug=True)
10+
11+
if __name__ == "__main__":
12+
main()

0 commit comments

Comments
 (0)