Skip to content

Commit 2ad7f53

Browse files
committed
[MZ] Finished inital app - first working version
1 parent 2ded548 commit 2ad7f53

10 files changed

Lines changed: 294 additions & 63 deletions

File tree

Dockerfile

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
FROM python:3.7-alpine
2-
3-
RUN apk update && apk add bash make automake gcc g++ subversion python3-dev
1+
FROM python:3.7.7-slim
42

53
# We copy just the requirements.txt first to leverage cache
64
COPY ./requirements.txt /app/requirements.txt
@@ -11,6 +9,6 @@ RUN python3 -m pip install -r requirements.txt
119

1210
COPY . /app
1311

14-
EXPOSE 3001
12+
EXPOSE 3001/udp
1513

16-
CMD ["python3", "src/app.py"]
14+
ENTRYPOINT ["python3", "-u", "app.py"]

Makefile

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
all: sort-imports format type-check test
22

3-
.PHONY: sort-imports format type-check test
3+
.PHONY: sort-imports format type-check test build run
44

55
SHELL=/bin/bash
66
SHELLFLAGS=-euo pipefail -c
77

88
sort-imports:
9-
python3 -m isort --atomic app tests
9+
python3 -m isort --atomic app.py test_app.py
1010

1111
format:
12-
python3 -m black app tests
12+
python3 -m black app.py test_app.py
1313

1414
type-check:
15-
python3 -m mypy app
15+
python3 -m mypy app.py
1616

1717
test:
18-
python3 -m pytest -o log_cli=true tests
18+
python3 -m pytest -o log_cli=true test_app.py
19+
20+
build: all
21+
docker build --tag emoji-app:latest .
22+
23+
run:
24+
docker run --rm --name emoji-app -p 3001:3001/udp emoji-app:latest

README.md

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,46 @@ Install the required Python packages with:
2323

2424
pip3 install -r requirements.txt
2525

26-
2726
## Run the app
2827

29-
The `Dockerfile` can be used to build an image and deploy it locally:
30-
Make sure you have Docker installed:
28+
Make sure you have Docker installed locally:
3129

3230
docker -v
3331

34-
Build the image with:
32+
Build the image locally with:
3533

3634
docker build --tag emoji-app:latest .
3735

38-
Run the container locally:
36+
Or:
37+
38+
make build
39+
40+
Run the container locally with:
3941

40-
docker run --name emoji-app:latest -p 3001:3001 app
42+
docker run --rm --name emoji-app -p 3001:3001/udp emoji-app:latest
43+
44+
Or:
45+
46+
make run
47+
48+
Optionally, the following flags can be used: `--n`, `--r`, `--s` and `--h/-h`:
49+
50+
* `--n` Multiply number of emojis by n (`int`, default: `1`)
51+
* `--r` Disable the translation from keyword to emoji (`bool`, default: `False`)
52+
* `--s` Separator between each emoji (`str`, default: `""`)
53+
* `--h/-h` See usage information
54+
55+
For example:
56+
57+
docker run --rm --name emoji-app -p 3001:3001/udp emoji-app:latest --n 2 --r True --s "+"
4158

42-
You can now trigger the endpoint (`localhost:3001`), for example using `nc`.
59+
In another shell session, you can now trigger the endpoint (`0.0.0.0:3001`), for example using `nc`:
60+
61+
nc -u 0.0.0.0 3001
62+
63+
Send a message and hit enter:
4364

44-
<!-- TODO -->
45-
<!-- Include usage info -->
65+
2 :ok:
4666

4767
## Development and testing
4868

app.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/usr/bin/env python
2+
3+
import asyncio
4+
import logging
5+
from typing import Any, Union
6+
7+
import coloredlogs
8+
9+
from utils import create_parser
10+
11+
logger = logging.getLogger(__name__)
12+
coloredlogs.install(level="INFO")
13+
14+
15+
# App settings
16+
APP_PORT = 3001
17+
TIME_SERVE_SECONDS = 3600 # Serve for 1 hour
18+
19+
TRANSLATION_TABLE = {
20+
":thumbsup:": "👍",
21+
":thumbsdown:": "👎",
22+
":ok:": "👌",
23+
":crossed:": "🤞",
24+
}
25+
26+
# Need these for static type checking with mypy
27+
multiplier: int
28+
separator: str
29+
translation_toggle: bool
30+
31+
32+
# For further reference see https://docs.python.org/3.7/library/asyncio-protocol.html#udp-echo-client
33+
class EchoServerProtocol(asyncio.BaseProtocol):
34+
def __init__(self) -> None:
35+
super().__init__()
36+
37+
def connection_made(self, transport) -> None: # type: ignore
38+
self.transport = transport
39+
40+
def datagram_received(self, data, addr) -> None: # type: ignore
41+
message = data.decode()
42+
logger.debug(f"Received message: '{message}' from addr: {addr}")
43+
response = generate_response(message)
44+
logger.debug(f"Generated response: '{response}'")
45+
print(response)
46+
47+
48+
def generate_response(message: str) -> Union[str, Any]:
49+
logger.debug(f"Generating response for message: {message}")
50+
51+
try:
52+
params = message.split()
53+
int(params[0])
54+
except Exception as e:
55+
logger.error(f"Unknown command: {message}")
56+
return f"Unknown command: {message}"
57+
58+
if params[1] not in TRANSLATION_TABLE:
59+
logger.error(f"Unknown command: {message}")
60+
return f"Unknown command: {message}"
61+
62+
number = int(params[0]) * multiplier
63+
text = TRANSLATION_TABLE[params[1]] if not translation_toggle else params[1]
64+
repsonse = number * [text]
65+
return separator.join(repsonse)
66+
67+
68+
async def start_udp_server(host: str = "0.0.0.0", port: int = APP_PORT) -> None:
69+
logger.info(f"Starting UDP server listening on: {host}:{port}")
70+
71+
loop = asyncio.get_running_loop()
72+
transport, protocol = await loop.create_datagram_endpoint(
73+
lambda: EchoServerProtocol(), local_addr=(host, port)
74+
)
75+
76+
try:
77+
await asyncio.sleep(TIME_SERVE_SECONDS)
78+
finally:
79+
transport.close()
80+
81+
82+
def main() -> None:
83+
parser = create_parser()
84+
args = parser.parse_args()
85+
logger.debug(f"Running with args: n: {args.n}, s: {args.s}, r: {args.r}")
86+
87+
global multiplier, separator, translation_toggle
88+
multiplier = args.n
89+
separator = args.s
90+
translation_toggle = args.r
91+
92+
asyncio.run(start_udp_server())
93+
94+
95+
if __name__ == "__main__":
96+
logger.debug(f"Running {__file__}")
97+
logger.debug(f"APP_PORT: {APP_PORT}")
98+
main()

app/app.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

app/config.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

app/utils.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

test_app.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env python
2+
3+
import unittest
4+
5+
import app
6+
7+
8+
class AppTestCase(unittest.TestCase):
9+
def test_base_case_thumbsup(self) -> None:
10+
app.multiplier = 1
11+
app.separator = ""
12+
app.translation_toggle = False
13+
self.assertEqual(app.generate_response("1 :thumbsup:"), "👍")
14+
15+
def test_base_case_thumbsdown(self) -> None:
16+
app.multiplier = 1
17+
app.separator = ""
18+
app.translation_toggle = False
19+
self.assertEqual(app.generate_response("2 :thumbsdown:"), "👎👎")
20+
21+
def test_base_case_ok(self) -> None:
22+
app.multiplier = 1
23+
app.separator = ""
24+
app.translation_toggle = False
25+
self.assertEqual(app.generate_response("5 :ok:"), "👌👌👌👌👌")
26+
27+
def test_base_case_crossed(self) -> None:
28+
app.multiplier = 1
29+
app.separator = ""
30+
app.translation_toggle = False
31+
self.assertEqual(app.generate_response("10 :crossed:"), "🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞")
32+
33+
def test_separator_1_thumbsup(self) -> None:
34+
app.multiplier = 1
35+
app.separator = ","
36+
app.translation_toggle = False
37+
self.assertEqual(app.generate_response("1 :thumbsup:"), "👍")
38+
39+
def test_separator_2_thumbsup(self) -> None:
40+
app.multiplier = 1
41+
app.separator = ","
42+
app.translation_toggle = False
43+
self.assertEqual(app.generate_response("2 :thumbsup:"), "👍,👍")
44+
45+
def test_separator_3_thumbsup(self) -> None:
46+
app.multiplier = 1
47+
app.separator = ","
48+
app.translation_toggle = False
49+
self.assertEqual(app.generate_response("3 :thumbsup:"), "👍,👍,👍")
50+
51+
def test_no_translation_1_thumbsup(self) -> None:
52+
app.multiplier = 1
53+
app.separator = ""
54+
app.translation_toggle = True
55+
self.assertEqual(app.generate_response("1 :thumbsup:"), ":thumbsup:")
56+
57+
def test_no_translation_2_thumbsup(self) -> None:
58+
app.multiplier = 1
59+
app.separator = ""
60+
app.translation_toggle = True
61+
self.assertEqual(app.generate_response("2 :thumbsup:"), ":thumbsup::thumbsup:")
62+
63+
def test_no_translation_3_thumbsup(self) -> None:
64+
app.multiplier = 1
65+
app.separator = ""
66+
app.translation_toggle = True
67+
self.assertEqual(
68+
app.generate_response("3 :thumbsup:"), ":thumbsup::thumbsup::thumbsup:"
69+
)
70+
71+
def test_multiplier_2_thumbsup(self) -> None:
72+
app.multiplier = 2
73+
app.separator = ""
74+
app.translation_toggle = False
75+
self.assertEqual(app.generate_response("1 :thumbsup:"), "👍👍")
76+
77+
def test_multiplier_3_thumbsup(self) -> None:
78+
app.multiplier = 3
79+
app.separator = ""
80+
app.translation_toggle = False
81+
self.assertEqual(app.generate_response("1 :thumbsup:"), "👍👍👍")
82+
83+
def test_multiplier_5_thumbsup(self) -> None:
84+
app.multiplier = 5
85+
app.separator = ""
86+
app.translation_toggle = False
87+
self.assertEqual(app.generate_response("1 :thumbsup:"), "👍👍👍👍👍")
88+
89+
def test_complex_ok_1(self) -> None:
90+
app.multiplier = 2
91+
app.separator = "++"
92+
app.translation_toggle = False
93+
self.assertEqual(app.generate_response("2 :ok:"), "👌++👌++👌++👌")
94+
95+
def test_complex_ok_2(self) -> None:
96+
app.multiplier = 3
97+
app.separator = " "
98+
app.translation_toggle = True
99+
self.assertEqual(
100+
app.generate_response("2 :ok:"), ":ok: :ok: :ok: :ok: :ok: :ok:"
101+
)
102+
103+
def test_complex_ok_2(self) -> None:
104+
app.multiplier = 3
105+
app.separator = " "
106+
app.translation_toggle = True
107+
self.assertEqual(
108+
app.generate_response("2 :ok:"), ":ok: :ok: :ok: :ok: :ok: :ok:"
109+
)
110+
111+
def test_failure(self) -> None:
112+
val = "x"
113+
self.assertEqual(app.generate_response(val), f"Unknown command: {val}")
114+
115+
def test_failure(self) -> None:
116+
val = "x :ok:"
117+
self.assertEqual(app.generate_response(val), f"Unknown command: {val}")
118+
119+
def test_failure(self) -> None:
120+
val = "1 :okokokokok:"
121+
self.assertEqual(app.generate_response(val), f"Unknown command: {val}")
122+
123+
124+
if __name__ == "__main__":
125+
unittest.main()

tests/test_app.py

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)