mirror of
https://github.com/autistic-symposium/web3-starter-py.git
synced 2025-06-04 21:29:32 -04:00
💾
This commit is contained in:
parent
7cb7a479f6
commit
69bb4175f1
124 changed files with 20 additions and 15 deletions
0
web2_projects/maze-puzzle/.gitkeep
Normal file
0
web2_projects/maze-puzzle/.gitkeep
Normal file
62
web2_projects/maze-puzzle/README.md
Normal file
62
web2_projects/maze-puzzle/README.md
Normal file
|
@ -0,0 +1,62 @@
|
|||
# Follower Maze Code Challenge Instructions
|
||||
|
||||
Follower-Maze is a **client-server application** built for social networking.
|
||||
Users follow other users, post messages, send private messages, unfollow users, and so on.
|
||||
* Each of these actions is an _event_, created by an _event-source_ and sent to the _server_.
|
||||
* Each event is processed by the _server_.
|
||||
* The processing of the event may have follow-on effects on other _user-clients_.
|
||||
|
||||
The **server** listens for two kinds of **clients**:
|
||||
* **1 event source**, which emits events.
|
||||
* **N user-clients** that connect to the server.
|
||||
|
||||
|
||||
#### Client/Server Protocol
|
||||
|
||||
* The protocol is line-based i.e. a LF control character terminates each message.
|
||||
All strings are encoded in UTF-8.
|
||||
* Data is transmitted over TCP sockets.
|
||||
* The _event source_ connects on port 9090 and will start sending events as soon as the connection is accepted.
|
||||
* The _user clients_ connect on port 9099 and identify themselves with their user ID.
|
||||
For example, once connected a _user client_ may send down `2932\r\n`,
|
||||
indicating that it is representing user 2932.
|
||||
* After the identification is sent,
|
||||
the user client starts waiting for events to be sent to them by the server.
|
||||
|
||||
#### Events
|
||||
|
||||
There are five possible events.
|
||||
The table below describes payloads sent by the event source and what each represents:
|
||||
|
||||
```
|
||||
| Payload | Sequence # | Type | From User Id | To User Id |
|
||||
|---------------|------------|---------------|--------------|------------|
|
||||
| 666|F|60|50 | 666 | Follow | 60 | 50 |
|
||||
| 1|U|12|9 | 1 | Unfollow | 12 | 9 |
|
||||
| 542532|B | 542532 | Broadcast | - | - |
|
||||
| 43|P|32|56 | 43 | Private Msg | 32 | 56 |
|
||||
| 634|S|32 | 634 | Status Update | 32 | - |
|
||||
```
|
||||
|
||||
#### Event Rules
|
||||
|
||||
* Each message begins with a sequence number.
|
||||
* However, the event source _does not send events in any given order_.
|
||||
In particular, sequence number has no effect on the order in which events are sent.
|
||||
* Events _may_ generate notifications for _user clients_.
|
||||
If there is a respective target _user client_ connected,
|
||||
the notification is routed according to the following rules:
|
||||
* Follow: Only the `To User Id` is notified
|
||||
* A follow event from user A to user B means that user A now follows user B.
|
||||
* Unfollow: No clients are notified
|
||||
* An unfollow event from user A to user B means that user A stopped following user B.
|
||||
* Broadcast: All connected _user clients_ are notified
|
||||
* Private Message: Only the `To User Id` is notified
|
||||
* Status Update: All current followers of the `From User ID` are notified
|
||||
* If there are no _user clients_ connected for a user,
|
||||
any notifications for them are currently ignored.
|
||||
_User clients_ are notified of events _in the correct sequence-number order_,
|
||||
regardless of the order in which the _event source_ sent them.
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
"""
|
||||
Interview note:
|
||||
This is a client "stub", here for demonstration and documentation
|
||||
purposes only.
|
||||
|
||||
In a real production environment, this client would send requests over
|
||||
the network to a product detail backend service.
|
||||
|
||||
The product detail backend service is responsible for retrieving product
|
||||
detail records from its datastore (e.g., a mysql database), and
|
||||
returning them to callers.
|
||||
"""
|
||||
class ProductDetailClient:
|
||||
|
||||
def lookup(product_token):
|
||||
"""
|
||||
Given a product token, makes a blocking call to the lookup endpoint of
|
||||
the product detail backend service,
|
||||
and returns a corresponding product detail record, if found.
|
||||
If no record is found, returns None.
|
||||
|
||||
Example result:
|
||||
|
||||
{"token": "AAA",
|
||||
"description": "Red Bike",
|
||||
"price_cents": 45000}
|
||||
"""
|
||||
pass
|
||||
|
||||
def batch_lookup(product_tokens):
|
||||
"""
|
||||
Given a list of product tokens, makes a blocking call to the
|
||||
batch lookup endpoint of the product detail backend service,
|
||||
and returns a map of product token to product detail record, for all
|
||||
that are found.
|
||||
Any records that cannot be found are ommitted from the result.
|
||||
|
||||
Example result:
|
||||
|
||||
{"AAA": {
|
||||
"token": "AAA",
|
||||
"description": "Red Bike",
|
||||
"price_cents": 45000},
|
||||
"BBB": {
|
||||
"token": "BBB",
|
||||
"description": "Blue Bike",
|
||||
"price_cents": 37500}}
|
||||
"""
|
||||
pass
|
|
@ -0,0 +1,44 @@
|
|||
"""
|
||||
Interview note:
|
||||
This is a client "stub", here for demonstration and documentation
|
||||
purposes only.
|
||||
|
||||
In a real production environment, this client would send requests over
|
||||
the network to a product search backend service.
|
||||
|
||||
The product search backend service is responsible for performing lookups
|
||||
against a product search index (e.g., elasticsearch) and returning product
|
||||
tokens that are search result "hits".
|
||||
"""
|
||||
class ProductSearchClient:
|
||||
|
||||
def search_for_all_in_price_range(lower_cents, upper_cents, start_record, num_records):
|
||||
"""
|
||||
Given a price range plus pagination information,
|
||||
returns a list of product tokens of products
|
||||
whose price falls in the range.
|
||||
|
||||
The price range is inclusive - that is,
|
||||
a given price "p" matches if:
|
||||
|
||||
upper_cents >= p >= lower_cents
|
||||
|
||||
If no products have prices in the price range,
|
||||
returns an empty list.
|
||||
|
||||
Product tokens are ordered by price, from lowest to highest.
|
||||
|
||||
Out of the total list of possible product tokens,
|
||||
only the "page" of product tokens starting at the list position
|
||||
start_record, and ending at start_record+num_records, are returned.
|
||||
|
||||
If start_record+num_records is greater than the number of actual
|
||||
result records, the resulting list size will be (accordingly)
|
||||
smaller than num_records.
|
||||
|
||||
Example result:
|
||||
|
||||
["AAA",
|
||||
"BBB"]
|
||||
"""
|
||||
pass
|
43
web2_projects/maze-puzzle/src/service.py
Normal file
43
web2_projects/maze-puzzle/src/service.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
import re
|
||||
|
||||
TOKEN_REGEX = re.compile("^[0-9A-F-]+$")
|
||||
|
||||
class Service:
|
||||
def __init__(self, product_detail_client, product_search_client):
|
||||
self.product_detail_client = product_detail_client
|
||||
self.product_search_client = product_search_client
|
||||
|
||||
def lookup(self, product_token):
|
||||
"""
|
||||
Given a valid product token,
|
||||
lookup product detail information from the product detail backend service.
|
||||
|
||||
- Results in Bad Request if the token is invalid
|
||||
- Results in Not Found if the product is unknown to the backend service
|
||||
"""
|
||||
if not TOKEN_REGEX.match(product_token):
|
||||
return {"status": 400}
|
||||
|
||||
lookup_result = self.product_detail_client.lookup(product_token)
|
||||
if lookup_result:
|
||||
return {"status": 200, "product": lookup_result}
|
||||
else:
|
||||
return {"status": 404}
|
||||
|
||||
def search_by_price_range(self, lower_cents, upper_cents, start_record, num_records):
|
||||
page_of_product_tokens = \
|
||||
self.product_search_client.search_for_all_in_price_range(
|
||||
lower_cents,
|
||||
upper_cents,
|
||||
start_record,
|
||||
num_records)
|
||||
|
||||
product_detail_results = []
|
||||
for product_token in page_of_product_tokens:
|
||||
product_detail_results.append(self.lookup(product_token)["product"])
|
||||
|
||||
return {"status": 200, "products": product_detail_results}
|
||||
|
||||
if __name__ == "__main__":
|
||||
a = Service()
|
||||
print(a.lookup('aaa'))
|
95
web2_projects/maze-puzzle/tests/service_test.py
Normal file
95
web2_projects/maze-puzzle/tests/service_test.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../src"))
|
||||
|
||||
from service import Service
|
||||
|
||||
|
||||
class ProductDetailClientTestDouble:
|
||||
def __init__(self):
|
||||
self.contents = {}
|
||||
|
||||
def lookup(self, product_token):
|
||||
if product_token in self.contents:
|
||||
return self.contents[product_token]
|
||||
else:
|
||||
return None
|
||||
|
||||
class ProductSearchClientTestDouble:
|
||||
def __init__(self):
|
||||
self.search_to_results = {}
|
||||
|
||||
def search_for_all_in_price_range(self, lower_cents, upper_cents, start_record, num_records):
|
||||
return self.search_to_results[(lower_cents, upper_cents, start_record, num_records)]
|
||||
|
||||
class ServiceTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.detail_client = ProductDetailClientTestDouble()
|
||||
self.search_client = ProductSearchClientTestDouble()
|
||||
self.service = Service(self.detail_client, self.search_client)
|
||||
|
||||
def test_lookup_success(self):
|
||||
expected_product = {
|
||||
"token": "AAA",
|
||||
"description": "Red Bike",
|
||||
"price_cents": 45000
|
||||
}
|
||||
|
||||
self.detail_client.contents["AAA"] = expected_product
|
||||
|
||||
self.assertEqual(
|
||||
self.service.lookup("AAA"),
|
||||
{"status": 200,
|
||||
"product": expected_product})
|
||||
|
||||
def test_lookup_not_found(self):
|
||||
self.assertEqual(
|
||||
self.service.lookup("AAA"),
|
||||
{"status": 404})
|
||||
|
||||
def test_lookup_bad_input(self):
|
||||
self.assertEqual(
|
||||
self.service.lookup("this-is-not-a-uuid"),
|
||||
{"status": 400})
|
||||
|
||||
def test_search_price_range(self):
|
||||
brown_bike = {
|
||||
"token": "AAA",
|
||||
"description": "Brown Bike",
|
||||
"price_cents": 10000
|
||||
}
|
||||
|
||||
blue_bike = {
|
||||
"token": "BBB",
|
||||
"description": "Blue Bike",
|
||||
"price_cents": 37500
|
||||
}
|
||||
|
||||
red_bike = {
|
||||
"token": "CCC",
|
||||
"description": "Red Bike",
|
||||
"price_cents": 45000
|
||||
}
|
||||
|
||||
gray_bike = {
|
||||
"token": "DDD",
|
||||
"description": "Gray Bike",
|
||||
"price_cents": 99900
|
||||
}
|
||||
|
||||
self.detail_client.contents["AAA"] = brown_bike
|
||||
self.detail_client.contents["BBB"] = blue_bike
|
||||
self.detail_client.contents["CCC"] = red_bike
|
||||
self.detail_client.contents["DDD"] = gray_bike
|
||||
|
||||
self.search_client.search_to_results[(30000, 50000, 0, 10)] = ["BBB", "CCC"]
|
||||
|
||||
self.assertEqual(
|
||||
self.service.search_by_price_range(30000, 50000, 0, 10),
|
||||
{"status": 200,
|
||||
"products": [blue_bike, red_bike]})
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
Add table
Add a link
Reference in a new issue