Skip to content

Commit

Permalink
Merge branch 'professional' of https://github.com/flaxandteal/python-…
Browse files Browse the repository at this point in the history
…course into professional
  • Loading branch information
philtweir committed Apr 24, 2021
2 parents 08ed6f3 + f0b2620 commit 6e971cd
Show file tree
Hide file tree
Showing 91 changed files with 1,679 additions and 17 deletions.
File renamed without changes.
36 changes: 26 additions & 10 deletions 006-latency-continued/tests/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,33 @@ def test_runner_can_generate_callers(config, run_cb):

# Don't generally need (or should) test private methods - they're internal concerns,
# but should test combined functionality
with patch('threading.Thread') as thread_mock, patch('threading.Lock') as thread_lock:
callers = runner.generate_callers(caller_indices, run_cb, create_client, results)
# with patch('threading.Thread') as thread_mock, patch('threading.Lock') as thread_lock:
callers = runner.generate_callers(caller_indices, run_cb, create_client, results)

assert type(callers) is dict
assert thread_mock.call_count == len(caller_indices)

# Could be more precise and check exact calls all match
thread_mock.assert_called_with(
target=runner.run_caller,
args=(run_cb, create_client, thread_lock(), results, config)
)
# note the scoping surprise above... (lock)
assert all([type(identifier) is str for identifier in callers])

# ...but may be better to let the real threads run with fake functions...
for idx, thread in callers.items():
thread.index = idx
thread.start()
thread.join()


def test_runner_threads_require_an_index(config, run_cb):
runner = ParallelRunner(config)

results = []

# Ensure we have our identifiers as strings, as these will be treated as names.
caller_indices = ['1', '2', '3', 'test']
create_client = lambda config: None

# Don't generally need (or should) test private methods - they're internal concerns,
# but should test combined functionality
# with patch('threading.Thread') as thread_mock, patch('threading.Lock') as thread_lock:
callers = runner.generate_callers(caller_indices, run_cb, create_client, results)

for thread in callers.values():
thread.start()
thread.join()
2 changes: 2 additions & 0 deletions 006-latency/bad_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
def a(t, b):
global n
n += 1
value = t(b)
print(str(n).strip()+": ",t(b))

def print(a,something, close=0):
global STDOUT
if STDOUT is None:
Expand Down
22 changes: 20 additions & 2 deletions 006-latency/network_test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,25 @@
repeats = 100 # Keep this <= 100, please!
timeout = 5 # Number of seconds until giving up on connection

def round_trip(skt):
payload = os.urandom(1024)

# IN HERE WE WILL WRITE THE NETWORK LATENCY CODE
skt.sendall(payload)
received_payload = skt.recv(1024)

logger.info("Average time taken: {delay} ms".format(delay=average_return_time))
if received_payload != payload:
raise IOError("We received an incorrect echo")

try:
with socket.create_connection(address=(host, port), timeout=timeout) as skt:
logger.info("Created connection")
round_trip(skt)
logger.info("Completed trial")
except ConnectionRefusedError as e:
logger.error(
"We could not create a socket connection to the "
"remote echo server"
)
raise e

# logger.info("Average time taken: {delay} ms".format(delay=average_return_time))
27 changes: 23 additions & 4 deletions 006-latency/network_test_client2.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import threading
import logging
import time

# Set up the logging
logging.basicConfig(
Expand All @@ -17,11 +18,29 @@

thread_count = 10 # Keep this <= 10, please!

data = []

lock = threading.Lock()

# Our in-thread routine
def run():
pass

# THREADING CONTROL CODE HERE
current_thread = threading.currentThread()

time.sleep(0.3)

with lock:
data.append(current_thread.index)

logging.info("Hi, my name is %s and %s", current_thread.getName(), current_thread.index)

thread_indices = range(1, thread_count + 1)
threads = {i: threading.Thread(target=run) for i in thread_indices}

for idx, thread in threads.items():
thread.index = idx
thread.start()

for thread in threads.values():
thread.join()

logging.info("COMPLETE")
logging.info("COMPLETE: %s", str(data))
3 changes: 3 additions & 0 deletions 021-conical-domain/magnum_opus/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.egg-info
.tox
Pipfile
23 changes: 23 additions & 0 deletions 021-conical-domain/magnum_opus/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM python:3-alpine

RUN addgroup -S user && adduser user -S -G user

WORKDIR /home/user/

COPY requirements.txt .
COPY gunicorn_config.py .
COPY setup.py .
COPY setup.cfg .

RUN pip install gunicorn
RUN pip install -r requirements.txt

USER user

EXPOSE 5000

ENTRYPOINT []

CMD gunicorn --config ./gunicorn_config.py magnumopus.index:app

COPY magnumopus magnumopus
1 change: 1 addition & 0 deletions 021-conical-domain/magnum_opus/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

5 changes: 5 additions & 0 deletions 021-conical-domain/magnum_opus/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Magnum Opus
===========

This recipe maker
and averaged over repeated attempts, returning an average round trip time.
9 changes: 9 additions & 0 deletions 021-conical-domain/magnum_opus/RULES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
* One unit of each of the substances Mercury, Salt and Sulphur are mixed, using my "Alembic" (mixing pot), giving one unit of another substance, Gloop
* Any attempt to mix anything other than those three substances, gives Sludge, another substance
* Substances can undergo several Processes in my Alembic - they can be Cooked, Washed, Pickled or Fermented
* If Gloop is Cooked, Washed, Pickled and Fermented, in that order, it is the Philosopher's Stone (panacea and cure of all ills)
[* To process a Substance, at least one unit must be in my Pantry, including Gloop - even when freshly processed/created, it must be stored there before re-use (to cool)]

Final rule:
GROUP 1: When I process a substance, using any process, it becomes a different substance
GROUP 2: When I process a substance, its state changes but is essentially the same substance (NB: mixing is not a process)
6 changes: 6 additions & 0 deletions 021-conical-domain/magnum_opus/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: "3"
services:
web:
build: .
ports:
- 5000:5000
5 changes: 5 additions & 0 deletions 021-conical-domain/magnum_opus/gunicorn_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
port = '5000'
bind = "0.0.0.0:%s" % port
workers = 1
timeout = 600
reload = False
Empty file.
51 changes: 51 additions & 0 deletions 021-conical-domain/magnum_opus/magnumopus/alembic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from .substance import Substance

class NotEnoughSubstancesToMixException(Exception):
pass

class UnknownProcessException(Exception):
pass


MIXTURES = {
('Mercury', 'Salt', 'Sulphur'): 'Gloop'
}


class Alembic:
_nature_of_unknown_mixture = 'Sludge'

@staticmethod
def _produce(nature):
return Substance(nature=nature)

def mix(self, *substances):
if len(substances) < 2:
raise NotEnoughSubstancesToMixException()

constituents = [substance.nature for substance in substances]

# This gives us a canonical, ordered way of expressing our
# constituents that we can use as a recipe look-up
ingredient_list = tuple(sorted(constituents))

try:
nature = MIXTURES[ingredient_list]
except KeyError:
nature = self._nature_of_unknown_mixture

return self._produce(nature)

def process(self, process_name, substance):
if process_name == 'ferment':
result = substance.ferment()
elif process_name == 'cook':
result = substance.cook()
elif process_name == 'wash':
result = substance.wash()
elif process_name == 'pickle':
result = substance.pickle()
else:
raise UnknownProcessException()

return result
3 changes: 3 additions & 0 deletions 021-conical-domain/magnum_opus/magnumopus/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import create_app

app = create_app()
5 changes: 5 additions & 0 deletions 021-conical-domain/magnum_opus/magnumopus/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import logging

def init_app(app):
app.logger.addHandler(logging.StreamHandler())
app.logger.setLevel(logging.INFO)
5 changes: 5 additions & 0 deletions 021-conical-domain/magnum_opus/magnumopus/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .base import db

def init_app(app):
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db.init_app(app)
3 changes: 3 additions & 0 deletions 021-conical-domain/magnum_opus/magnumopus/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
23 changes: 23 additions & 0 deletions 021-conical-domain/magnum_opus/magnumopus/models/substance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class SubstanceMustBeFreshToProcessException(Exception):
pass

class Substance:
def __init__(self, nature='Unknown'):
self.nature = nature
self.state = []

def _process(self, process_name):
self.state.append(process_name)
return self

def cook(self):
return self._process('cooked')

def pickle(self):
return self._process('pickled')

def ferment(self):
return self._process('fermented')

def wash(self):
return self._process('washed')
12 changes: 12 additions & 0 deletions 021-conical-domain/magnum_opus/magnumopus/pantry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Pantry:
def __init__(self):
self._cupboard = []

def add_substance(self, substance):
self._cupboard.append(substance)

def find_substance_by_nature(self, nature):
return [substance for substance in self._cupboard if substance.nature == nature][0]

def count_all_substances(self):
return len(self._cupboard)
Empty file.
14 changes: 14 additions & 0 deletions 021-conical-domain/magnum_opus/magnumopus/repositories/pantry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class Pantry:
_cupboard = []

def __init__(self):
pass

def add_substance(self, substance):
self._cupboard.append(substance)

def find_substances_by_nature(self, nature):
return [substance for substance in self._cupboard if substance.nature == nature]

def count_all_substances(self):
return len(self._cupboard)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from flask_restful import Api
from . import substance
from . import alembic_instruction

def init_app(app):
api = Api(app)
substance.init_app(app, api)
alembic_instruction.init_app(app, api)
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from flask_restful import Resource, reqparse
from ..repositories.pantry import Pantry
from ..models.substance import Substance
from ..schemas.substance_schema import SubstanceSchema
from ..services.alembic_instruction_handler import AlembicInstructionHandler, AlembicInstruction

parser = reqparse.RequestParser()
parser.add_argument('instruction_type')
parser.add_argument('action')
parser.add_argument('natures')

substance_schema = SubstanceSchema()

class AlembicInstructionResource(Resource):
def get(self):
"""This should return past requests/commands."""
pass

def post(self):
"""
Add an instruction for the alembic.
Note that POST is _not_ assumed to be idempotent, unlike PUT
"""

args = parser.parse_args()
instruction_type = args['instruction_type']

pantry = Pantry()

instruction_handler = AlembicInstructionHandler()

# This could do with deserialization...
instruction = AlembicInstruction(
instruction_type=args.instruction_type,
natures=args.natures.split(','),
action=args.action
)

# Crude start at DI... see flask-injector
result = instruction_handler.handle(instruction, pantry)

pantry.add_substance(result)

return substance_schema.dump(result)


def init_app(app, api):
api.add_resource(AlembicInstructionResource, '/alembic_instruction')
Loading

0 comments on commit 6e971cd

Please sign in to comment.