├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md └── qualifier ├── qualifier.py └── tests.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 86 | # in version control: https://pdm.fming.dev/#use-with-ide 87 | .pdm.toml 88 | 89 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 90 | __pypackages__/ 91 | 92 | # Celery stuff 93 | celerybeat-schedule 94 | celerybeat.pid 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # Environments 100 | .env 101 | .venv 102 | env/ 103 | venv/ 104 | ENV/ 105 | env.bak/ 106 | venv.bak/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | .dmypy.json 121 | dmypy.json 122 | 123 | # Pyre type checker 124 | .pyre/ 125 | 126 | # pytype static type analyzer 127 | .pytype/ 128 | 129 | # Cython debug symbols 130 | cython_debug/ 131 | 132 | # PyCharm 133 | .idea/ 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Python Discord 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Summer Code Jam 2022: Qualifier 2 | 3 | To qualify for the upcoming Summer Code Jam, you'll have to complete a qualifier assignment. The goal is to make sure you have enough Python knowledge to effectively contribute to a team. 4 | 5 | Please read the rules and instructions carefully, and submit your solution before the deadline using the [sign-up form](https://forms.pythondiscord.com/form/cj9-qualifier). 6 | 7 | # Table of Contents 8 | 9 | - [Qualifying for the Code Jam](#qualifying-for-the-code-jam) 10 | - [Rules and Guidelines](#rules-and-guidelines) 11 | - [Qualifier Assignment: The Dirty Fork](#qualifier-assignment-the-dirty-fork) 12 | - [Restaurant Protocol](#restaurant-protocol) 13 | - [The Recipe for a Restaurant](#the-recipe-for-a-restaurant) 14 | - [Step 1 - On Duty](#step-1---on-duty) 15 | - [Step 2 - Off Duty](#step-2---off-duty) 16 | - [Step 3 - Handling Customers](#step-3---handling-customers) 17 | - [Step 4 - Order Specialization](#step-4---order-specialization) 18 | 19 | # Qualifying for the Code Jam 20 | 21 | To qualify for the Code Jam you will be required to upload your submission to the [sign-up form](https://forms.pythondiscord.com/form/cj9-qualifier). 22 | We set up our test suite so you don't have to worry about setting one up yourself. 23 | 24 | Your code will be tested with a multitude of tests to test all aspects of your code making sure it works. 25 | 26 | # Rules and Guidelines 27 | 28 | - Your submission will be tested using a Python 3.10.5 interpreter without any additional packages installed. You're allowed to use everything included in Python's standard library, but nothing else. Please make sure to include the relevant `import` statements in your submission. 29 | 30 | - Use [`qualifier.py`](qualifier/qualifier.py) as the base for your solution. It includes a stub for the class you need to write: `RestaurantManager`. 31 | 32 | - Do not change the **signature** of functions included in [`qualifier.py`](qualifier/qualifier.py), and do not change the `Request` class. The test suite we will use to judge your submission relies on them. Everything else, including the docstring, may be changed. 33 | 34 | - Do not include "debug" code in your submission. You should remove all debug prints and other debug statements before you submit your solution. 35 | 36 | - This qualifier task is supposed to be **an individual challenge**. You should not discuss (parts of) your solution in public (including our server), or rely on others' solutions to the qualifier. Failure to meet this requirement may result in the **disqualification** of all parties involved. You are still allowed to do research and ask questions about Python as they relate to your qualifier solution, but try to use general examples if you post code along with your questions. 37 | 38 | - You can run the tests locally by running the `unittest` suite with `python -m unittest tests.py` or `py -m unittest tests.py` from within the 39 | `./qualifier` directory. 40 | 41 | # Qualifier Assignment: The Dirty Fork 42 | 43 | The Python Discord group is joining the hype of on-demand food delivery services. Our new online restaurant is called “The Dirty Fork”. 44 | > “The lemon chicken I ordered arrived quickly and hot. My delivery driver Dave was just ducky! I highly recommend this service!” 45 | > 46 | >⠀⠀⠀⠀⠀⠀⠀⠀— Mr. Hem J. Lock 47 | 48 | We would like you to create an application that takes in orders from customers, and delegates them to on-duty staff. Once the staff is done, your application should serve the finished order to the customer. 49 | 50 | ## Restaurant Protocol 51 | In [`qualifier.py`](qualifier/qualifier.py) there is a template to start with; read the docstrings to understand what each method does. 52 | 53 | ## The Recipe for a Restaurant 54 | ### Step 1 - On Duty 55 | Before the day begins, all staff members will send a request to the application. You can identify this by looking at the `request.scope` dictionary; the `"type"` key will be set to `"staff.onduty"`. With each staff member there will also be an ID included so that they can be identified. 56 | 57 | An example `"staff.onduty"` request: 58 | ```json 59 | { 60 | "type": "staff.onduty", 61 | "id": "AbCd3Fg", 62 | "speciality": ["meat"] 63 | } 64 | ``` 65 | 66 | When a `"staff.onduty"` request is received, you should add their request to the `self.staff` dictionary using their ID as the key. This is so that we can keep track of who is currently working. 67 | 68 | There is also a "speciality" key included, but you do not need to worry about that yet. 69 | 70 | ### Step 2 - Off Duty 71 | At the end of the day, staff members will let your application know that they are going off-duty. This will be done with a new Request. You can identify an off-duty request by the Request scope key `"type"` — it will be set to `"staff.offduty"`. 72 | 73 | An example `"staff.offduty"` request: 74 | ```json 75 | { 76 | "type": "staff.offduty", 77 | "id": "AbCd3Fg" 78 | } 79 | ``` 80 | 81 | When a `"staff.offduty"` request is received, the staff member must be removed from the `self.staff` dictionary, as they will no longer be accepting food orders. 82 | 83 | > **Note** 84 | > We will only test staff going off-duty after all orders are complete, but of course in a real application that might not be the case. 85 | 86 | ### Step 3 - Handling Customers 87 | After all staff members have become on-duty, you will begin receiving requests from customers trying to order food. 88 | 89 | Requests from customers can be identified by the Request's scope dictionary's `"type"` key having the value `"order"`. 90 | ```json 91 | { 92 | "type": "order", 93 | "speciality": "meat" 94 | } 95 | ``` 96 | 97 | When an order request is received, you should receive the full order via the `.receive()` method. Your application doesn't need to concern itself with what this order is. This object should just be: 98 | - Passed to a selected member of staff by calling the `.send()` method. 99 | - Afterwards, call the staff's `.receive()` method to get the result. 100 | - And finally, pass the result back to the order using the `.send()` method. 101 | 102 | ```python 103 | found = ... # One selected member of staff 104 | 105 | full_order = await request.receive() 106 | await found.send(full_order) 107 | 108 | result = await found.receive() 109 | await request.send(result) 110 | ``` 111 | 112 | ### Step 4 - Order Specialization 113 | Each staff has a list of things they specialize in. You can read this from the staff's request `scope` dictionary with the `"speciality"` key (British spelling). 114 | 115 | Example requests: 116 | ```json 117 | { 118 | "type": "staff.onduty", 119 | "id": "AbCd3Fg", 120 | "speciality": ["pasta", "vegetables"] 121 | } 122 | ``` 123 | 124 | ```json 125 | { 126 | "type": "order", 127 | "speciality": "pasta" 128 | } 129 | ``` 130 | 131 | An order requires a certain specialty, which can be read via the order's `"speciality"` key in its `scope` dictionary. Your application should pass the order to a staff member that has the order's specialty. 132 | 133 | > **Note** 134 | > The `"speciality"` key is included in all `"staff.onduty"` requests, but absent from `"staff.offduty"` requests. 135 | 136 | #### Challenge Yourself 137 | We won't test you on how you distribute work between prioritized staff members, but in a self-respecting kitchen, work should be distributed fairly. 138 | 139 | > **Warning** 140 | > The tests rely on the structure of `self.staff`. If you wish to change the structure of the `self.staff` attribute at any point in this step, you can create a property named `staff` to make earlier tests still pass. It should return a dictionary with the same structure as `self.staff` used to. 141 | 142 | ## Good Luck! 143 | 144 | ![Event Banner](https://github.com/python-discord/branding/blob/main/jams/summer_code_jam_2022/site_banner.png?raw=true) 145 | -------------------------------------------------------------------------------- /qualifier/qualifier.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from dataclasses import dataclass 3 | 4 | 5 | @dataclass(frozen=True) 6 | class Request: 7 | scope: typing.Mapping[str, typing.Any] 8 | 9 | receive: typing.Callable[[], typing.Awaitable[object]] 10 | send: typing.Callable[[object], typing.Awaitable[None]] 11 | 12 | 13 | class RestaurantManager: 14 | def __init__(self): 15 | """Instantiate the restaurant manager. 16 | 17 | This is called at the start of each day before any staff get on 18 | duty or any orders come in. You should do any setup necessary 19 | to get the system working before the day starts here; we have 20 | already defined a staff dictionary. 21 | """ 22 | self.staff = {} 23 | 24 | async def __call__(self, request: Request): 25 | """Handle a request received. 26 | 27 | This is called for each request received by your application. 28 | In here is where most of the code for your system should go. 29 | 30 | :param request: request object 31 | Request object containing information about the sent 32 | request to your application. 33 | """ 34 | ... 35 | -------------------------------------------------------------------------------- /qualifier/tests.py: -------------------------------------------------------------------------------- 1 | import random 2 | import unittest 3 | import itertools 4 | from types import MappingProxyType 5 | from typing import Any, Awaitable, Callable, Dict, List 6 | from unittest.mock import AsyncMock 7 | 8 | import qualifier 9 | from qualifier import Request 10 | 11 | 12 | STAFF_IDS = ( 13 | "jmMZkSGVBbCDgKKMMSNPS", "HeLlOWoRlD123", "iKnowThatYouAreReadingThis", 14 | "PyTHonDIscorDCoDEJam", "iWAShereWRITINGthis" 15 | ) 16 | SPECIALITIES = ( 17 | "pasta", "meat", "vegetables", "non-food", "dessert", 18 | ) 19 | 20 | 21 | async def _receive() -> None: ... 22 | async def _send(_: object) -> None: ... 23 | 24 | 25 | class WarnTypoAccess(dict): 26 | def __getitem__(self, key): 27 | if key == "specialty": 28 | raise RuntimeError( 29 | "You may be using the wrong spelling for 'speciality'" 30 | "; The correct key name is 'speciality', not 'specialty'." 31 | ) 32 | return super().__getitem__(key) 33 | 34 | def get(self, key, default=None): 35 | if key == "specialty": 36 | raise RuntimeError( 37 | "You may be using the wrong spelling for 'speciality'" 38 | "; The correct key name is 'speciality', not 'specialty'." 39 | ) 40 | return super().get(key, default) 41 | 42 | 43 | def create_request( 44 | scope: Dict[str, Any], 45 | receive: Callable[[], Awaitable[object]] = _receive, 46 | send: Callable[[object], Awaitable[Any]] = _send 47 | ) -> Request: 48 | """ 49 | Create a request object with the given scope and receive/send functions. 50 | Raises an error with help message if 'specialty' is accessed. 51 | """ 52 | return Request(MappingProxyType(WarnTypoAccess(scope)), receive, send) 53 | 54 | 55 | def wrap_receive_mock(id_: str, mock: AsyncMock) -> Callable[[], Awaitable[object]]: 56 | async def receive() -> object: 57 | return await mock(id_) 58 | return receive 59 | 60 | 61 | def wrap_send_mock(id_: str, mock: AsyncMock) -> Callable[[object], Awaitable[Any]]: 62 | async def send(obj: object) -> Any: 63 | return await mock(id_, obj) 64 | return send 65 | 66 | 67 | class QualifierTestCase(unittest.IsolatedAsyncioTestCase): 68 | def setUp(self) -> None: 69 | self.manager = qualifier.RestaurantManager() 70 | 71 | def verify_staff_dict(self): 72 | self.assertTrue(hasattr(self.manager, "staff"), msg="Restaurant manager has no staff attribute") 73 | staff = self.manager.staff 74 | 75 | # This is safe against different hooks that isinstance() has 76 | self.assertIs(type(staff), dict, msg="'staff' attribute is not a dictionary") 77 | for key, value in staff.items(): 78 | self.assertIs(type(key), str, msg="Staff dictionary key is not a string") 79 | self.assertIs(type(value), Request, msg="Staff dictionary value is not a Request") 80 | 81 | 82 | class RegistrationTests(QualifierTestCase): 83 | """Test that the qualifier implemented Step 1 correctly.""" 84 | 85 | def test_manager_staff_dict(self): 86 | self.verify_staff_dict() 87 | 88 | async def test_staff_registration(self): 89 | id_ = STAFF_IDS[0] 90 | receive, send = AsyncMock(), AsyncMock() 91 | 92 | staff = create_request({"type": "staff.onduty", "id": id_, "speciality": [SPECIALITIES[0]]}, receive, send) 93 | 94 | await self.manager(staff) 95 | 96 | self.verify_staff_dict() # Manager may have overriden it after adding staff 97 | 98 | # These are separated to be more helpful when failing 99 | self.assertEqual(len(self.manager.staff), 1, msg="Not the correct amount of staff registered") 100 | self.assertIn(id_, self.manager.staff, msg="Staff not registered with the correct ID") 101 | self.assertEqual( 102 | self.manager.staff[id_], staff, 103 | msg="Staff request not stored as dictionary value" 104 | ) 105 | 106 | receive.assert_not_called() 107 | send.assert_not_called() 108 | 109 | receive.reset_mock() 110 | send.reset_mock() 111 | 112 | await self.manager(create_request({"type": "staff.offduty", "id": id_}, receive, send)) 113 | 114 | self.verify_staff_dict() 115 | 116 | self.assertEqual(self.manager.staff, {}, msg="Staff not removed after going off-duty") 117 | 118 | async def test_multiple_staff_registration(self) -> None: 119 | staff: List[Request] = [] 120 | 121 | for id_, speciality in zip(STAFF_IDS, SPECIALITIES): 122 | receive, send = AsyncMock(), AsyncMock() 123 | 124 | request = create_request({"type": "staff.onduty", "id": id_, "speciality": [speciality]}, receive, send) 125 | staff.append(request) 126 | 127 | await self.manager(request) 128 | 129 | self.verify_staff_dict() # Ensure it is still a dictionary for the following assertions 130 | 131 | self.assertEqual(len(self.manager.staff), len(STAFF_IDS), msg="Not all staff were registered") 132 | 133 | for id_, request in zip(STAFF_IDS, staff): 134 | with self.subTest(staff_id=id_): 135 | self.assertIn(id_, self.manager.staff, msg="Registered staff's ID not found in dictionary") 136 | self.assertEqual(self.manager.staff[id_], request, msg="Staff request not stored as dictionary value") 137 | 138 | request.receive.assert_not_called() 139 | request.send.assert_not_called() 140 | 141 | for id_, request in zip(STAFF_IDS, staff): 142 | with self.subTest(staff_id=id_): 143 | 144 | request.receive.reset_mock() 145 | request.send.reset_mock() 146 | 147 | await self.manager(create_request({"type": "staff.offduty", "id": id_}, request.receive, request.send)) 148 | 149 | self.verify_staff_dict() 150 | self.assertEqual(self.manager.staff, {}, msg="Not all staff removed after going off-duty") 151 | 152 | 153 | class DeliveringTests(QualifierTestCase): 154 | 155 | async def test_handle_customer(self) -> None: 156 | id_ = STAFF_IDS[-1] 157 | 158 | complete_order, result = object(), object() 159 | staff = create_request( 160 | {"type": "staff.onduty", "id": id_, "speciality": [SPECIALITIES[-1]]}, 161 | AsyncMock(return_value=result), AsyncMock() 162 | ) 163 | 164 | await self.manager(staff) 165 | 166 | order = create_request( 167 | {"type": "order", "speciality": SPECIALITIES[-1]}, 168 | AsyncMock(return_value=complete_order), AsyncMock() 169 | ) 170 | await self.manager(order) 171 | 172 | order.receive.assert_awaited_once() 173 | staff.send.assert_awaited_once_with(complete_order) 174 | 175 | staff.receive.assert_awaited_once() 176 | order.send.assert_awaited_once_with(result) 177 | 178 | await self.manager(create_request({"type": "staff.offduty", "id": id_})) 179 | 180 | async def test_handle_multiple_customers(self) -> None: 181 | # We cannot *necessarily* assume that there will be an even distribution of orders at 182 | # this point. We should decouple the testing of orders being delivered to staff, and 183 | # the testing of the distribution of those orders. 184 | 185 | # List of tuple with the first item being the order and the second 186 | # being the result. 187 | sentinels = [(object(), object()) for _ in range(len(STAFF_IDS))] 188 | 189 | # By reusing these we don't need to care about which staff was sent the order. 190 | staff_receive, staff_send = AsyncMock(), AsyncMock() 191 | staff = [ 192 | create_request( 193 | {"type": "staff.onduty", "id": id_, "speciality": [speciality]}, 194 | 195 | # We wrap the mocks so that they pass the ID of the staff, that way 196 | # we can ensure that the order was both sent and received to the same staff. 197 | wrap_receive_mock(id_, staff_receive), wrap_send_mock(id_, staff_send) 198 | ) 199 | for id_, speciality in zip(STAFF_IDS, reversed(SPECIALITIES)) 200 | ] 201 | 202 | for request in staff: 203 | await self.manager(request) 204 | 205 | orders = [ 206 | create_request({"type": "order", "speciality": speciality}, AsyncMock(), AsyncMock()) 207 | for speciality in SPECIALITIES 208 | ] 209 | 210 | for order, (full_order, result) in zip(orders, sentinels): 211 | order.receive.return_value = full_order 212 | staff_receive.return_value = result 213 | 214 | await self.manager(order) 215 | 216 | staff_send.assert_awaited_once() 217 | 218 | # We assert that it is 2 arguments, because the wrapper over the mock passes an additional one 219 | self.assertEqual( 220 | len(staff_send.call_args.args), 2, 221 | msg="Staff send method not awaited with correct amount of arguments" 222 | ) 223 | 224 | staff_id = staff_send.call_args.args[0] 225 | staff_send.assert_awaited_once_with(staff_id, full_order) 226 | 227 | # Make sure the same staff was also received from 228 | staff_receive.assert_awaited_once_with(staff_id) 229 | 230 | order.receive.assert_awaited_once_with() 231 | order.send.assert_awaited_once_with(result) 232 | 233 | staff_receive.reset_mock() 234 | staff_send.reset_mock() 235 | 236 | for request in staff: 237 | await self.manager(create_request({"type": "staff.offduty", "id": request.scope["id"]})) 238 | 239 | async def test_order_speciality_match(self) -> None: 240 | staff_ids, specialities = list(STAFF_IDS), list(SPECIALITIES) 241 | random.shuffle(staff_ids) 242 | random.shuffle(specialities) 243 | 244 | staff_receive, staff_send = AsyncMock(), AsyncMock() 245 | staff = { 246 | id_: create_request( 247 | {"type": "staff.onduty", "id": id_, "speciality": [speciality]}, 248 | 249 | # We wrap the mocks so that they pass the ID of the staff, that way 250 | # we can ensure that the order was both sent and received to the same staff. 251 | wrap_receive_mock(id_, staff_receive), wrap_send_mock(id_, staff_send) 252 | ) 253 | for id_, speciality in zip(staff_ids, specialities) 254 | } 255 | 256 | for request in staff.values(): 257 | await self.manager(request) 258 | 259 | orders = [create_request({"type": "order", "speciality": speciality}) for speciality in specialities * 10] 260 | 261 | for order in orders: 262 | await self.manager(order) 263 | 264 | staff_send.assert_awaited_once() 265 | staff_id = staff_send.call_args.args[0] 266 | 267 | self.assertIn( 268 | order.scope["speciality"], staff[staff_id].scope["speciality"], 269 | msg="Order speciality not matched with speciality of staff" 270 | ) 271 | staff_send.reset_mock() 272 | 273 | for request in staff.values(): 274 | await self.manager(create_request({"type": "staff.offduty", "id": request.scope["id"]})) 275 | 276 | async def test_uneven_order_speciality(self) -> None: 277 | # Similar to test_order_speciality_match() but there are multiple staff 278 | # with the same speciality. 279 | staff_ids, specialities = list(STAFF_IDS), list(SPECIALITIES[:2]) 280 | random.shuffle(staff_ids) 281 | random.shuffle(specialities) 282 | 283 | staff_receive, staff_send = AsyncMock(), AsyncMock() 284 | staff = { 285 | id_: create_request( 286 | {"type": "staff.onduty", "id": id_, "speciality": [speciality]}, 287 | 288 | # We wrap the mocks so that they pass the ID of the staff, that way 289 | # we can ensure that the order was both sent and received to the same staff. 290 | wrap_receive_mock(id_, staff_receive), wrap_send_mock(id_, staff_send) 291 | ) 292 | for id_, speciality in zip(staff_ids, itertools.cycle(specialities)) 293 | } 294 | 295 | for request in staff.values(): 296 | await self.manager(request) 297 | 298 | orders = [ 299 | create_request({"type": "order", "speciality": speciality}) 300 | for speciality in itertools.chain(*itertools.repeat(specialities, 5)) 301 | ] 302 | 303 | for order in orders: 304 | await self.manager(order) 305 | 306 | staff_send.assert_awaited_once() 307 | staff_id = staff_send.call_args.args[0] 308 | 309 | self.assertIn( 310 | order.scope["speciality"], staff[staff_id].scope["speciality"], 311 | msg="Order speciality not matched with speciality of staff" 312 | ) 313 | staff_send.reset_mock() 314 | 315 | for request in staff.values(): 316 | await self.manager(create_request({"type": "staff.offduty", "id": request.scope["id"]})) 317 | 318 | async def test_multiple_specialities(self) -> None: 319 | id_one, id_two = random.sample(STAFF_IDS, 2) 320 | 321 | staff_receive, staff_send = AsyncMock(), AsyncMock() 322 | 323 | staff_one = create_request( 324 | {"type": "staff.onduty", "id": id_one, "speciality": [SPECIALITIES[0]]}, 325 | wrap_receive_mock(id_one, staff_receive), 326 | wrap_send_mock(id_one, staff_send) 327 | ) 328 | await self.manager(staff_one) 329 | 330 | staff_two = create_request( 331 | {"type": "staff.onduty", "id": id_two, "speciality": SPECIALITIES[1:]}, 332 | wrap_receive_mock(id_two, staff_receive), 333 | wrap_send_mock(id_two, staff_send) 334 | ) 335 | await self.manager(staff_two) 336 | 337 | orders = [ 338 | create_request({"type": "order", "speciality": speciality}) 339 | for speciality in itertools.chain(*itertools.repeat(SPECIALITIES, 5)) 340 | ] 341 | 342 | for order in orders: 343 | await self.manager(order) 344 | 345 | staff_send.assert_awaited_once() 346 | staff_id = staff_send.call_args.args[0] 347 | if order.scope["speciality"] == SPECIALITIES[0]: 348 | self.assertEqual(staff_id, staff_one.scope["id"], msg="Order speciality not match with speciality of staff") 349 | else: 350 | self.assertEqual(staff_id, staff_two.scope["id"], msg="Order speciality not match with speciality of staff") 351 | 352 | staff_send.reset_mock() 353 | --------------------------------------------------------------------------------