This commit is contained in:
osiris account 2023-03-12 15:29:57 -07:00
parent 7cb7a479f6
commit 69bb4175f1
124 changed files with 20 additions and 15 deletions

View file

View 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.

View file

@ -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

View file

@ -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

View 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'))

View 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()