Quantcast
Channel: Planet Python
Viewing all 23137 articles
Browse latest View live

Bruno Rocha: Dynaconf 1.0.x released - Layered configuration system for python with flask and django support

$
0
0

Dynaconf

dynaconf - The dynamic configurator for your Python Project

MIT LicensePyPIPyPITravis CIcodecovCodacy grade

dynaconf a layered configuration system for Python applications - with strong support for 12-factor applications and extensions for Flask and Django.

Features

  • Strict separation of settings from code (following 12-factor applications Guide).
  • Define comprehensive default values.
  • Store parameters in multiple file formats (.toml, .json, .yaml, .ini and .py).
  • Sensitive secrets like tokens and passwords can be stored in safe places like .secrets file or vault server.
  • Parameters can optionally be stored in external services like Redis server.
  • Simple feature flag system.
  • Layered [environment] system.
  • Environment variables can be used to override parameters.
  • Support for .env files to automate the export of environment variables.
  • Correct data types (even for environment variables).
  • Have only one canonical settings module to rule all your instances.
  • Drop in extension for Flaskapp.config object.
  • Drop in extension for Djangoconf.settings object.
  • Powerful $ dynaconf CLI to help you manage your settings via console.
  • Customizable Validation System to ensure correct config parameters.
  • Allow the change of dynamic parameters on the fly without the need to redeploy your application.
  • Easily extensible
  • 100% test coverage
  • 100% documented

Read the Full Documentation at: http://dynaconf.readthedocs.io/

Repository: http://github.com/rochacbruno/dynaconf/

Getting Started

Installation

Python 3.x is required

$ pip install dynaconf

Default installation supports .toml, .py and .json file formats and also environment variables (.env supported)

Usage

Accessing config variables in your Python application

In your Python program wherever you need to access a settings variable you use the canonical object from dynaconf import settings:

NOTE: Read the full documentation for more examples like using Dynaconf with Flask or Django

Example of program to connect to some database

from some.db import Client

from dynaconf import settings               # The only object you need to import

conn = Client(
    username=settings.USERNAME,             # attribute style access
    password=settings.get('PASSWORD'),      # dict get style access
    port=settings['PORT'],                  # dict item style access
    timeout=settings.as_int('TIMEOUT'),     # Forcing casting if needed
    host=settings.get('HOST', 'localhost')  # Providing defaults
)

Where the settings values are stored

Dynaconf aims to have a flexible and usable configuration system. Your applications can be configured via a configuration files, through environment variables, or both. Configurations are separated into environments: [development], [staging], [testing] and [production]. The working environment is selected via an environment variable.

Sensitive data like tokens, secret keys and password can be stored in .secrets.* files and/or external storages like Redis or vault secrets server.

Besides the built-in optional support to redis as settings storage dynaconf allows you to create custom loaders and store the data wherever you want e.g: databases, memory storages, other file formats, nosql databases etc.

Working environments

At any point in time, your application is operating in a given configuration environment. By default there are four such environments:

  • [development]
  • [staging]
  • [testing]
  • [production]

You can also define [custom environment] and use the pseudo-envs [default] to provide comprehensive default values and [global] to provide global values to overrride in any other environment.

Without any action, your applications by default run in the [development] environment. The environment can be changed via the ÈNV_FOR_DYNACONF environment variable. For example, to launch an application in the [staging] environment, we can run:

export ENV_FOR_DYNACONF=staging

or

ENV_FOR_DYNACONF=staging python yourapp.py

NOTE: When using FLask Extension the environment can be changed via FLASK_ENV variable and for Django Extension you can use DJANGO_ENV.

The settings files

NOTE:Read the full documentaion about dynaconf CLI to learn how to automatically create the settings files for your project.

An optional settings.{toml|py|json|ini|yaml} file can be used to specify the configuration parameters for each environment. If it is not present, only the values from environment variables are used (.env file is also supported). Dynaconf searches for the file starting at the current working directory. If it is not found there, Dynaconf checks the parent directory. Dynaconf continues checking parent directories until the root is reached.

The recommended file format is TOML but you can choose to use any of .{toml|py|json|ini|yaml}.

The file must be a series of sections, at most one for [default], optionally one for each [environment], and an optional [global] section. Each section contains key-value pairs corresponding to configuration parameters for that [environment]. If a configuration parameter is missing, the value from [default] is used. The following is a complete settings.toml file, where every standard configuration parameter is specified within the [default] section:

NOTE: if the file format choosen is .py as it does not support sections you can create multiple files like settings.py for [default], development_settings.py, production_settings.py and global_settings.py. ATTENTION using .py is not recommended for configuration use TOML!

[default]
username = "admin"
port = 5000
host = "localhost"
message = "default message"
value = "default value"

[development]
username = "devuser"

[staging]
host = "staging.server.com"

[testing]
host = "testing.server.com"

[production]
host = "server.com"

[awesomeenv]
value = "this value is set for custom [awesomeenv]"

[global]
message = "This value overrides message of default and other envs"

The [global] pseudo-environment can be used to set and/or override configuration parameters globally. A parameter defined in a [global] section sets, or overrides if already present, that parameter in every environment. For example, given the following settings.toml file, the value of address will be "1.2.3.4" in every environment:

[global]
address = "1.2.3.4"

[development]
address = "localhost"

[production]
address = "0.0.0.0"

NOTE: The [env] name and first level variables are case insensitive as internally dynaconf will always use upper case, that means [development] and [DEVELOPMENT] are equivalent and address and ADDRESS are also equivalent. This rule does not apply for inner data structures as dictionaries and arrays.

Supported file formats

By default toml is the recommended format to store your configuration, however you can switch to a different supported format.

# If you wish to include support for more sources
pip3 install dynaconf[yaml|ini|redis|vault]

# for a complete installation
pip3 install dynaconf[all]

Once the support is installed no extra configuration is needed to load data from those files, dynaconf will search for settings files in the root directory of your application looking for the following files in the exact order below:

DYNACONF_LOADING_ORDER = [
 'settings.py',
 '.secrets.py',
 'settings.toml',
 '.secrets.toml',
 'settings.yaml',
 '.secrets.yaml',
 'settings.ini',
 '.secrets.ini',
 'settings.json',
 '.secrets.json',
 # redis server if REDIS_ENABLED_FOR_DYNACONF=true
 # vault server if VAULT_ENABLED_FOR_DYNACONF=true
 # other sources if custom loaders are defined
 # All environment variables prefixed with DYNACONF_
]

NOTE: Dynaconf works in an layered override mode based on the above order, so if you have multiple file formats with conflicting keys defined, the precedence will be based on the loading order.

Take a look at the example folder to see some examples of use with different file formats.

Sensitive secrets

Using .secrets files

To safely store sensitive data Dynaconf also searches for a .secrets.{toml|py|json|ini|yaml} file to look for data like tokens and passwords.

example .secrets.toml:

[default]
password = "sek@987342$"

The secrets file supports all the environment definitions supported in the settings file.

IMPORTANT: The reason to use a .secrets.* file is the ability to omit this file when commiting to the repository so a recommended .gitignore should include .secrets.* line.

Using Vault server

The vaultproject.io/ is a key:value store for secrets and Dynaconf can load variables from a Vault secret.

  1. Run a vault server

Run a Vault server installed or via docker:

$ docker run -d -e 'VAULT_DEV_ROOT_TOKEN_ID=myroot' -p 8200:8200 vault
  1. Install support for vault in dynaconf
$ pip install dynaconf[vault]
  1. In your .env file or in exported environment variables define:
VAULT_ENABLED_FOR_DYNACONF=true
VAULT_URL_FOR_DYNACONF="http://localhost:8200"
VAULT_TOKEN_FOR_DYNACONF="myroot"

Now you can have keys like PASSWORD and TOKEN defined in the vault and dynaconf will read it.

To write a new secret you can use http://localhost:8200 web admin and write keys under the /secret/dynaconf secret database.

You can also use the Dynaconf writer via console

$ dynaconf write vault -s password=123456

Environment variables

overloading parameters via env vars

All configuration parameters, including custom environments and dynaconf configuration, can be overridden through environment variables.

To override the configuration parameter {param}, use an environment variable named DYNACONF_{PARAM}. For instance, to override the "HOST" configuration parameter, you can run your application with:

DYNACONF_HOST='otherhost.com' python yourapp.py

.env files

If you don't want to declare the variables on every program call you can run export DYNACONF_{PARAM} in your shell or put the values in a .env file located in the same directory as your settings files (the root directory of your application), variables in .env does not overrride existing environment variables.

IMPORTANT: Dynaconf will search for a .env located in the root directory of your application, if not found it will continue searching in parent directories until it reaches the root. To avoid conflicts we recommend to have a .env even if it is empty.

Precedence and type casting

Environment variables take precedence over all other configuration sources: if the variable is set, it will be used as the value for the parameter even if parameter exists in settings files or in .env.

Variable values are parsed as if they were TOML syntax. As illustration, consider the following examples:

# Numbers
DYNACONF_INTEGER=42
DYNACONF_FLOAT=3.14

# Text
DYNACONF_STRING=Hello
DYNACONF_STRING="Hello"

# Booleans
DYNACONF_BOOL=true
DYNACONF_BOOL=false

# Use extra quotes to force a string from other type
DYNACONF_STRING="'42'"
DYNACONF_STRING="'true'"

# Arrays must be homogenous in toml syntax
DYNACONF_ARRAY=[1, 2, 3]
DYNACONF_ARRAY=[1.1, 2.2, 3.3]
DYNACONF_ARRAY=['a', 'b', 'c']

# Dictionaries
DYNACONF_DICT={key="abc",val=123}

# toml syntax does not allow `None/null` values so use @none
DYNACONF_NONE='@none None'

# toml syntax does not allow mixed type arrays so use @json
DYNACONF_ARRAY='@json [42, 3.14, "hello", true, ["otherarray"], {"foo": "bar"}]'

NOTE: Older versions of Dynaconf used the @casting prefixes for env vars like export DYNACONF_INTEGER='@int 123' still works but this casting is deprecated in favor of using TOML syntax described above. To disable the @casting do export AUTO_CAST_FOR_DYNACONF=false

The global prefix

The DYNACONF_{param} prefix is set by GLOBAL_ENV_FOR_DYNACONF and serves only to be used in environment variables to override config values.

This prefix itself can be changed to something more significant for your application, however we recommend kepping DYNACONF_{param} as your global env prefix.

NOTE: See the Configuring dynaconf section in documentation to learn more on how to use .env variables to configure dynaconf behavior.

Flask Extension

Dynaconf provides a drop in replacement for app.config.

As Flask encourages the composition by overriding the config_class attribute this extension follows the patterns of Flask and turns your Flask's app.config in to a dynaconf instance.

Initialize the extension

Initialize the FlaskDynaconf extension in your app

from flask import Flask
from dynaconf import FlaskDynaconf

app = Flask(__name__)
FlaskDynaconf(app)

You can optionally use init_app as well.

Use FLASK_ environment variables

Then the app.config will work as a dynaconf.settings instance and FLASK_ will be the global prefix for exporting environment variables.

Example:

export FLASK_DEBUG=true              # app.config.DEBUG
export FLASK_INTVALUE=1              # app.config['INTVALUE']
export FLASK_MAIL_SERVER='host.com'  # app.config.get('MAIL_SERVER')

Settings files

You can also have settings files for your Flask app, in the root directory (the same where you execute flask run) put your settings.toml and .secrets.toml files and then define your environments [default], [development] and [production].

To switch the working environment the FLASK_ENV variable can be used, so FLASK_ENV=development to work in development mode or FLASK_ENV=production to switch to production.

IMPORTANT: To use $ dynaconf CLI the FLASK_APP must be defined.

IF you don't want to manually create your config files take a look at the CLI

Django Extension

Dynaconf a drop in replacement to django.conf.settings.

Following this pattern recommended pattern this extension makes your Django's conf.settings in to a dynaconf instance.

Initialize the extension

In your django project's settings.py include:

INSTALLED_APPS = [
    'dynaconf.contrib.django_dynaconf',
    ...
]

NOTE: The extension must be included as the first INSTALLED_APP of the list

Use DJANGO_ environment variables

Then django.conf.settings will work as a dynaconf.settings instance and DJANGO_ will be the global prefix to export environment variables.

Example:

export DJANGO_DEBUG=true     # django.conf.settings.DEBUG
export DJANGO_INTVALUE=1     # django.conf.settings['INTVALUE]
export DJANGO_HELLO="Hello"  # django.conf.settings.get('HELLO)

Settings files

You can also have settings files for your Django app, in the root directory (the same where manage.py is located) put your settings.toml and .secrets.toml files and then define your environments [default], [development] and [production].

To switch the working environment the DJANGO_ENV variable can be used, so DJANGO_ENV=development to work in development mode or DJANGO_ENV=production to switch to production.

IMPORTANT: To use $ dynaconf CLI the DJANGO_SETTINGS_MODULE must be defined.

IF you don't want to manually create your config files take a look at the CLI

NOTE: It is recommended that all the django's internal config vars should be kept in the settings.py of your project, then application specific values you can place in dynaconf's settings.toml in the root (same folder as manage.py). You can override settings.py values in the dynaconf settings file as well.

How to contribute

In github repository issues and Pull Request are welcomed!

  • New implementations
  • Bug Fixes
  • Bug reports
  • More examples of use in /example folder
  • Documentation
  • Feedback as issues and comments ot joining dynaconf on Telegram or #dynaconf on freenode
  • Donation to rochacbruno [at] gmail.com in PayPal

Read the docs

Documentation: http://dynaconf.readthedocs.io/

Repository: http://github.com/rochacbruno/dynaconf/


Sandipan Dey: Few Machine Learning Problems (with Python implementations)

$
0
0
In this article a few machine learning problems from a few online courses will be described.   1. Fitting the distribution of heights data This problem appeared as an assignment problem in the coursera course Mathematics for Machine Learning: Multivariate Calculus. The description of the problem is taken from the assignment itself. In this assessment … Continue reading Few Machine Learning Problems (with Python implementations)

Vasudev Ram: Porting a prime checker from Q to Python (with improvements)

$
0
0
By Vasudev Ram


Q => Py

Hi readers,

I was checking out the Q programming language.

It's an interesting language, a descendant of APL, that supports array and functional programming styles. Some information about Q from its Wikipedia page:

Paradigm: Array, functional
Designed by: Arthur Whitney
Developer: Kx Systems
First appeared: 2003
Typing discipline: Dynamic, strong
Influenced by: A+, APL, Scheme, K

Q is a proprietary array processing language developed by Arthur Whitney and commercialized by Kx Systems. The language serves as the query language for kdb+, a disk based and in-memory, column-based database. kdb+ is based upon K, a terse variant of APL. Q is a thin wrapper around K, providing a more readable, English-like interface.

I had tried out Q a bit recently, using one of the tutorials.

Then, while reading the Q Wikipedia page again, I saw this paragraph:

[
When x is an integer greater than 2, the following function will return 1 if it is a prime, otherwise 0:

{min x mod 2_til x}
The function is evaluated from right to left:

"til x" enumerate the positive integers less than x.
"2_" drops the first two elements of the enumeration (0 and 1).
"x mod" performs modulo division between the original integer and each value in the truncated list.
"min" find the minimum value of the list of modulo result.
]

It's basically an algorithm in Q to find whether a given number x (> 2) is prime or not [1]. The algorithm returns 1 if x is a prime, else 0. Notice how concise it is. That conciseness is a property of the APL family of languages, such as APL, J and Q. In fact Q is slightly less concise than some of the others, I've read, because it puts a thin English-like wrapper on the hard-core APL symbolic syntax. But all of them are still very concise.

So I thought of implementing that algorithm in Python, just for fun. I first wrote a naive version, more or less a port of the Q version. Then I rewrote that first version in a more functional way. Then I realized that there are other opportunities for improving the code [2], and implemented a few of them.

So I combined the few different versions of the is_prime_* functions (where * = 1, 2, 3, etc.) that I had written, in a single program, with a driver function to exercise all of them. The code is in file is_prime.py, shown further below. There are comments in the program that explain the logic and improvements or differences of the various is_prime function versions.

[1] Prime_number

[2] There are obviously many other ways of checking if numbers are prime, and many of them are faster than these approaches; see [3]. These are not among the most efficient ways, or even close; I was just experimenting with different ways of rewriting and refactoring the code, after the initial port from Q to Python. Even the original Q version in the Wikipedia page was not meant to be a fast version, since it does not use any of the improvements I mention below, let alone using any special Q-specific algorithm or language feature, or advanced general prime-finding algorithms.

[3] Primality test

There might still be some scope for further changes or improvements, some of which I may do in a future post. A few such improvements are:

1. Don't divide x by all values up to x - 1. Instead, only check up to the square root of x. This is a standard improvement often taught to programming beginners; in fact, it is mentioned in the Wikipedia articles about primes.

2. Terminate early when the first remainder equal to 0 is found. This avoids unnecessarily computing all the other remainders. I did that in is_prime_3().

3. Don't check for divisibility by any even number > 2 (call it b), because if any such b divides x evenly, then so will 2, and we would have checked earlier if 2 divides x evenly.

4. Create a function for the output statements, most of which are common across the different is_prime versions, and call that function instead from those places.

5. Use a generator to lazily yield divisors, and when any divisor gives a zero remainder, exit early, since it means the number is not prime. Done in is_prime_4().

Other ways of doing it may include use of some other functional programming features of Python such as filter(), itertools.takewhile/dropwhile, etc. (The itertools module has many functions and is very interesting, but that is a subject for a different post.)

I also observed some interesting behavior when running the program with large ranges of inputs for prime number checking. Will analyze that a bit and write about my opinions on that in a future post.

Here is the code for is_prime.py:
# File: isprime.py
# A port of a primality checking algorithm from the Q language to Python,
# plus a few improvements / variations, using Python features.
# Ref: https://en.wikipedia.org/wiki/Q_(programming_language_from_Kx_Systems)
# Search for the word "prime" in that page to see the Q code.

# Author: Vasudev Ram
# Copyright 2018 Vasudev Ram
# Web site: https://vasudevram.github.io
# Blog: https://jugad2.blogspot.com
# Product store: https://gumroad.com/vasudevram

from __future__ import print_function
from debug1 import debug1

import sys

# Naive, mostly procedural port from the Q version.
def is_prime_1(x):
# Guard against invalid argument.
assert x > 2, "in is_prime_1: x > 2 failed"
# The range of integers from 0 to x - 1
til_x = range(x)
# The range without the first 2 items, 0 and 1.
divs = til_x[2:]
# The remainders after dividing x by each integer in divs.
mods = map(lambda d: x % d, divs)
# x is prime if the minimum-valued remainder equals 1.
return min(mods) == 1

# Shorter, more functional version, with nested calls
# to min, map and range.
def is_prime_2(x):
assert x > 2, "in is_prime_2: x > 2 failed"
# Eliminate slicing used in is_prime_1, just do range(2, x).
return min(map(lambda d: x % d, range(2, x))) == 1

# Early-terminating version, when 1st remainder equal to 0 found,
# using a list for the range of divisors.
def is_prime_3(x):
assert x > 2, "in is_prime_3: x > 2 failed"
divs = range(2, x)
# Check if x is divisible by any integer in divs; if so,
# x is not prime, so terminate early.
debug1("in is_prime_3, x", x)
for div in divs:
debug1(" in loop, div", div)
if x % div == 0:
return False
# If we reach here, x was not divisible by any integer in
# 2 to x - 1, so x is prime.
return True

# Generator function to yield the divisors one at a time, to
# avoid creating the whole list of divisors up front.
def gen_range(start, x):
assert start > 0, "in gen_range, start > 0 failed"
assert x > start, "in gen_range, x > start failed"
i = start
while i < x:
yield i
i += 1

# Early-terminating version, when 1st remainder equal to 0 found,
# using a generator for the range of divisors.
def is_prime_4(x):
assert x > 2, "in is_prime_4, x > 2 failed"
divs = gen_range(2, x)
debug1("in is_prime_4, x", x)
for div in divs:
debug1(" in loop, div", div)
if x % div == 0:
return False
return True

def check_primes(low, high):
assert low <= high, "in check_primes, low <= high failed"

"""
print("\nWith a for loop:")
for x in range(low, high + 1):
print(x, "is" if is_prime_1(x) else "is not", "prime,", end="")
print()
"""

print("\nWith nested function calls:")
output = [ str(x) + (" prime" if is_prime_2(x) else " not prime") \
for x in range(low, high + 1) ]
print(", ".join(output))

print("\nWith a list of divisors and early termination:")
output = [ str(x) + (" prime" if is_prime_3(x) else " not prime") \
for x in range(low, high + 1) ]
print(", ".join(output))

print("\nWith a generator of divisors and early termination:")
output = [ str(x) + (" prime" if is_prime_4(x) else " not prime") \
for x in range(low, high + 1) ]
print(", ".join(output))

def main():
try:
low = int(sys.argv[1])
high = int(sys.argv[2])
if low <= 2:
print("Error: Low value must be > 2.")
sys.exit(1)
if high < low:
print("Error: High value must be >= low value.")
sys.exit(1)
print("Checking primality of integers between {} and {}".format(low, high))
check_primes(low, high)
sys.exit(0)
except ValueError as ve:
print("Caught ValueError: {}".format(str(ve)))
except IndexError as ie:
print("Caught IndexError: {}".format(str(ie)))
sys.exit(1)

if __name__ == '__main__':
main()
And here are some runs of the output, below, both for normal and error cases. Note that I used my debug1() debugging utility function in a few places, to show what divisors are being used, in a few places. This helps show that the early termination logic works. To turn off debugging output, simply use the -O option, like this example:

python -O is_prime.py other_args

This improved version of the debug1 function (get it here), unlike the earlier version that was shown in this blog post:

A simple Python debugging function

, does not require the user to set any environment variables like VR_DEBUG, since it uses Python's built-in __debug__ variable instead. So to enable debugging, nothing extra needs to be done, since that variable is set to True by default. To disable debugging, all we have to do is pass the -O option on the python command line.

Here is the prime program's output:

Try this one later (if you're trying out the program), since it takes 
longer to run. You may observe some interesting behavior:

$ python -O is_prime.py 3 10000 | less

where less is the Unix 'less' command, a text pager. Any command-line text pager
that can read standard input (from a pipe) will work.

Try some of these below (both normal and error cases) before the one above:

Some error-handling cases:

$ python -O is_prime.py 0 0
Error: Low value must be > 2.

$ python -O is_prime.py 2 0
Error: Low value must be > 2.

$ python -O is_prime.py 3 0
Error: High value must be >= low value.

$ python -O is_prime.py 4 2
Error: High value must be >= low value.

Some normal cases:

$ python -O is_prime.py 3 3
Checking primality of integers between 3 and 3

With nested function calls:
3 prime

With a list of divisors and early termination:
3 prime

With a generator of divisors and early termination:
3 prime

To show that the early termination logic works, run the program without
the -O option.
Here is one such run. Due to more debugging output, I've only checked
two numbers, 4 and 5. But you can try with any number of values if you
page the output, or redirect it to a file.

$ python is_prime.py 4 5
Checking primality of integers between 4 and 5

With nested function calls:
4 not prime, 5 prime

With a list of divisors and early termination:
in is_prime_3, x: 4
in loop, div: 2
in is_prime_3, x: 5
in loop, div: 2
in loop, div: 3
in loop, div: 4
4 not prime, 5 prime

With a generator of divisors and early termination:
in is_prime_4, x: 4
in loop, div: 2
in is_prime_4, x: 5
in loop, div: 2
in loop, div: 3
in loop, div: 4
4 not prime, 5 prime

You can see from the above run that for 4, the checking stops early,
at the first divisor (2), in fact, because it evenly divides 4.
But for 5, all divisors from 2 to 4 are checked, because 5 has
no prime factors (except itself and 1).

And here is a run checking for primes between 3 and 30:

$ python -O is_prime.py 3 30
Checking primality of integers between 3 and 30

With nested function calls:
3 prime, 4 not prime, 5 prime, 6 not prime, 7 prime, 8 not prime, 9 not prime,
10 not prime, 11 prime, 12 not prime, 13 prime, 14 not prime, 15 not prime,
16 not prime, 17 prime, 18 not prime, 19 prime, 20 not prime, 21 not prime,
22 not prime, 23 prime, 24 not prime, 25 not prime, 26 not prime, 27 not prime,
28 not prime, 29 prime, 30 not prime

With a list of divisors and early termination:
3 prime, 4 not prime, 5 prime, 6 not prime, 7 prime, 8 not prime, 9 not prime,
10 not prime, 11 prime, 12 not prime, 13 prime, 14 not prime, 15 not prime,
16 not prime, 17 prime, 18 not prime, 19 prime, 20 not prime, 21 not prime,
22 not prime, 23 prime, 24 not prime, 25 not prime, 26 not prime, 27 not prime,
28 not prime, 29 prime, 30 not prime

With a generator of divisors and early termination:
3 prime, 4 not prime, 5 prime, 6 not prime, 7 prime, 8 not prime, 9 not prime,
10 not prime, 11 prime, 12 not prime, 13 prime, 14 not prime, 15 not prime,
16 not prime, 17 prime, 18 not prime, 19 prime, 20 not prime, 21 not prime,
22 not prime, 23 prime, 24 not prime, 25 not prime, 26 not prime, 27 not prime,
28 not prime, 29 prime, 30 not prime

You can see that all three functions (is_prime_2 to 4) give the same results.
(I commented out the call to the naive function version, is_prime_1, after
a few runs (not shown), so none of these outputs shows its results, but they
are the same as the others, except for minor formatting differences, due to
the slightly different output statements used.

I also timed the program for finding primes up to 1000 and 10,000
(using my own simple command timing program written in Python - not shown).

Command: python -O is_prime.py 3 1000
Time taken: 2.79 seconds
Return code: 0

Command: python -O is_prime.py 3 10000
Time taken: 66.28 seconds
Return code: 0

Related links:

Q (programming_language from Kx_Systems

Kx Systems

Kdb+

Column-oriented DBMS

In-memory database

If you want to try out Q, Kx Systems a free version available for download for non-commercial use, here: Q downloads

Enjoy.

- Vasudev Ram - Online Python training and consulting

Get fast web hosting with A2Hosting.com

Get updates (via Gumroad) on my forthcoming apps and content.

Jump to posts: Python * DLang * xtopdf

Subscribe to my blog by email

My ActiveState Code recipes

Follow me on: LinkedIn * Twitter

Are you a blogger with some traffic? Get Convertkit:

Email marketing for professional bloggers



Programiz: Python Shallow Copy and Deep Copy

$
0
0
In this article, you’ll learn about shallow copy and deep copy in Python with the help of examples.

Talk Python to Me: #164 Python in Brain Research at the Allen Institute

$
0
0
The brain is truly one of the final frontiers of human exploration. Understanding how brains work has vast consequences for human health and computation. Imagine how computers might change if we actually understood how thinking and even consciousness worked. On this episode, you'll meet Justin Kiggins and Corinne Teeter who are research scientists using Python for their daily work at the Allen Institute for Brain Science. They are joined by Nicholas Cain who is a software developer supporting scientists there using Python as well.

Tryton News: Newsletter June 2018

$
0
0

The development of Tryton is back on the road after the release of the 4.8 series. Here are the major changes of this last month.

A Sunny June Iowa DayCC BY 2.0 Carl Wycoff

Changes for the user

Allow to edit account move maturity date

The maturity date represents information which has no impact on the accounting but on business only. So it is more flexible to allow to edit it.

Invoice shipment costs only, if there is a shipment

When the cost method is on order and the invoice method is on shipment, the shipment cost is only invoiced when a shipment is done. Before there was an issue with this configuration.

Same input and storage location for customer return shipment

Following similar changes for customer and supplier shipments, the customer return shipments support also to use the same input and storage location. In such case, on reception the shipment is directly marked as done.

URL in web client

The web client sao now supports URLs. This allows users to communicate information by sharing URLs. The syntax is quite similar to the one of the desktop client since the 'index.html' has been removed but uses a URL-hash instead of the full path due to the single page design. The URL is updated to correspond to the active tab. The hash part can be changed by the user (using copy/paste) to open the corresponding tab. This allows to navigate back in the history of the browser. The client parses the URL hash on login and launches the corresponding action.

Web client session

The sessions are now stored in the localStorage of the browser. This means that the session per server and database can be shared between tabs and survive a reload of the page.

Better space management in web client

We have improved the navigation bar to provide more spaces to the tab list. We use an icon for the logout instead of a longer text. Now the favorites menu is a drop down of the global search. The toggle menu and Tryton brand are merged. Also it is now possible to toggle the menu for any size of the screen.

web client with more spaces

Open Many2One and Reference in new web client tab

It is now possible to open the target record in a new tab instead of a popup by pressing the CTRL key when clicking on the "open" button. This gives access to the actions, relates and reports for this record like the right-click on desktop client does.

Removal of single windows executable

Since we have the web client sao, the need of a Windows executable without installation is very low. More over it required administration rights to be run. So we have decided to stop publishing it in future versions.

Uninstall previous version

On windows, the installer required to uninstall previous version before allowing to install a new one. In order to simplify the process, now it asks to uninstall the previous version of the same series. In silent mode, this is done automatically without any required intervention of the user which ease automatic deployment.

Export CSV

The export CSV window has been improved by moving the predefined exports under the buttons that manage them. This way the view is easier to understand and the predefined list is not the default focused widget (which created inconsistent selection state).

Export CSV predefined desktop windows

Display current date on document

Draft documents, that are sent to customer, have not yet a date. For example a sale quotation does not have the sale date filled but the document should have at least the date when it was printed to define when starts the validity of the quotation. So we changed the templates to print the current date if no date is yet filled.

Changes for the developer

Add sql_cast on Field

The new method Field.sql_cast(expression) provides the database specific cast of the field values in SQL expressions.

Recursive common table expression for child_of/parent_of operators

For the evaluation of the child_of and parent_of domain operator, we used recursive ORM loops (or Modified Preorder Tree Traversal when available). Now all supported databases support the recursive common table expression (CTE), so we could use it as default implementation. Regarding performance recursive CTE is not better than MPTT but still avoid many round-trip compared to the recursive ORM loop.

Tree Mixin

We added a new Mixin as core feature. It provides all the needed features to manage record names in a tree structure and to check against loops. All the modules having such design have been updated to benefit from this generic implementation that is fully tested.

Limit database per host name

The Tryton server works on multiple database names (one way to support multi-company). But in shared environment, the list of database names can be very large and can leak information. For that, we added a new feature which allows to filter the list of database names per host name.

Default language in report

Until now, almost all reports were using English as fallback language because the call to set_lang required a language code and it was easier to hard-code 'en' than retrieving the default language of the database. Now the set_lang method allow as argument None or an instance of ir.lang. When the value is None, it uses the default language of the database. This is a more consistent behavior with the rest of the application. We have updated all the reports to use this new feature.

Extending depends on methods

The depends on methods was limited to only on_change or on_change_with methods. But it showed some limitations when trying the share common code between different on_change/_with methods. So we extended and generalized the behavior. Now we can define dependencies on any methods and use the @fields.depends decorator on any method.

Django Weblog: Django bugfix release: 2.0.6

$
0
0

Today we've issued the 2.0.6 bugfix release.

The release package and checksums are available from our downloads page, as well as from the Python Package Index. The PGP key ID used for this release is Carlton Gibson: E17DF5C82B4F9D00.

Bhishan Bhandari: Appium Python Client

$
0
0

Using Appium with Python Appium is an open source test automation framework for use with native, hybrid and mobile web apps. It drives iOS, Android, and Windows apps using the WebDriver protocol. While the main purpose of appium is to perform automation testing, it can be utilized for variety of other things too. Appium has […]

The post Appium Python Client appeared first on The Tara Nights.


The Digital Cat: A game of tokens: write an interpreter in Python with TDD - Part 4

$
0
0

In the first three parts of this series of posts we developed together a calculator using a pure TDD methodology. In the previous post we added support for variables.

In this new post we will first add the exponentiation operation. The operator will be challenging because it has a high priority, so we will need to spice up the peek functions to look at multiple tokens.

Then I will show you how I performed a refactoring of the code introducing a new version of the lexer that greatly simplifies the code of the parser.

Level 15 - Exponentiation

That is power.

The exponentiation operation is simple, and Python uses the double star operator to represent it

>>> 2**3
8

The main problem that we will face implementing it is the priority of such an operation. Traditionally, this operator has precedence on the basic arithmetic operations (sum, difference, multiplication, division). So if I write

>>> 1 + 2 ** 3
9

Python correctly computes 1 + (2 ** 3) and not (1 + 2) ** 3. As we did with multiplication and division, then, we will need to create a specific step to parse this operation.

NOTE: I realised too late that the class called PowerNode should have been called ExponentiationNode, the former being a leftover of a previous incorrect nomenclature. I will eventually fix it in one of the refactoring steps, trying to convert a mistake into a good example of TDD.

Lexer

The lexer has a simple task, that of recognising the symbol '^' as a LITERAL token. The test goes into tests/test_calc_lexer.py

deftest_get_tokens_understands_exponentiation():l=clex.CalcLexer()l.load('2 ^ 3')assertl.get_tokens()==[token.Token(clex.INTEGER,'2'),token.Token(clex.LITERAL,'^'),token.Token(clex.INTEGER,'3'),token.Token(clex.EOL),token.Token(clex.EOF)]

Does your code already pass the test? If yes, why?

Parser

It's time to test the proper parsing of the exponentiation operation. Add this test to tests/test_calc_parser.py

deftest_parse_exponentiation():p=cpar.CalcParser()p.lexer.load("2 ^ 3")node=p.parse_exponentiation()assertnode.asdict()=={'type':'exponentiation','left':{'type':'integer','value':2},'right':{'type':'integer','value':3},'operator':{'type':'literal','value':'^'}}

As you can see this test checks directly the parse_exponentiation() method, so you just need to properly implement that, at this stage.

To allow the use of the exponentiation operator '^' in the calculator, however, we have to integrate it with the rest of the parse functions, so we will add three tests to the same file. The first one tests that the natural priority of the exponentiation operator is higher than the multiplication

deftest_parse_exponentiation_with_other_operators():p=cpar.CalcParser()p.lexer.load("3 * 2 ^ 3")node=p.parse_term()assertnode.asdict()=={'type':'binary','operator':{'type':'literal','value':'*'},'left':{'type':'integer','value':3},'right':{'type':'exponentiation','left':{'type':'integer','value':2},'right':{'type':'integer','value':3},'operator':{'type':'literal','value':'^'}}}

The second one checks that the parenthesis still change the priority

deftest_parse_exponentiation_with_parenthesis():p=cpar.CalcParser()p.lexer.load("(3 + 2) ^ 3")node=p.parse_term()assertnode.asdict()=={'type':'exponentiation','operator':{'type':'literal','value':'^'},'left':{'type':'binary','operator':{'type':'literal','value':'+'},'left':{'type':'integer','value':3},'right':{'type':'integer','value':2}},'right':{'type':'integer','value':3}}

And the third one checks that unary operators still have a higher priority than the exponentiation operator

deftest_parse_exponentiation_with_negative_base():p=cpar.CalcParser()p.lexer.load("-2 ^ 2")node=p.parse_exponentiation()assertnode.asdict()=={'type':'exponentiation','operator':{'type':'literal','value':'^'},'left':{'type':'unary','operator':{'type':'literal','value':'-'},'content':{'type':'integer','value':2}},'right':{'type':'integer','value':2}}

My advice is to add the first test and to try and pass that one changing the code. If your change doesn't touch too much of the existing parse methods, chances are that the following two tests will pass as well.

Visitor

Last, we need to properly expose the exponentiation operation in the CLI, which means to change the Visitor in order to support nodes of type 'exponentiation. The test that we need to add to tests/test_calc_visitor.py is

deftest_visitor_exponentiation():ast={'type':'exponentiation','left':{'type':'integer','value':2},'right':{'type':'integer','value':3},'operator':{'type':'literal','value':'^'}}v=cvis.CalcVisitor()assertv.visit(ast)==(8,'integer')

And the change to the CalcVisitor class should be very easy as we need to simply process a new type of node.

Intermezzo - Refactoring with tests

See? You just had to see it in context.

So our little project is growing, and the TDD methodology we are following gives us plenty of confidence in what we did. There as for sure bugs we are not aware of, we we are sure that the cases that we tested are correctly handled by our code.

As happens in many projects at a certain point it's time for refactoring. We implemented solutions to the problems that we found along the way, but are we sure we avoided duplicating code, that we chose the best solution for some algorithms, or more trivially that the names we chose for the variables are clear?

Refactoring means basically to change the internal structure of something without changing its external behaviour, and tests are a priceless help in this phase. The tests we wrote are there to ensure that the previous behaviour does not change. Or, if it changes, that we are perfectly aware of it.

In this section, thus, I want to guide you through a refactoring guided by tests. If you are following this series and writing your own code this section will not add anything to the project, but I recommend that you read it anyway, as it shows why tests are so important in a software project.

If you want to follow the refactoring on the repository you can create a branch on the tag context-manager-refactoring and work there. From that commit I implemented the steps you will find in the next sections.

Context managers

The main issue the current code has is that the lexer cannot automatically recover a past status, that is we cannot easily try to parse something and, when we discover that the initial guess is wrong, go back in time and start over.

Let's consider the parse_line() method

defparse_line(self):try:self.lexer.stash()returnself.parse_assignment()exceptclex.TokenError:self.lexer.pop()returnself.parse_expression()

Since a line can contain either an assignment or an expression the first thing this function does is to save the lexer status with stash() and try to parse an assignment. If the code is not an assignment somewhere is the code the TokenError exception is raised, and parse_line() restores the previous status of the lexer and tries to parse an expression.

The same thing happens in other methods like parse_term()

defparse_term(self):left=self.parse_exponentiation()next_token=self.lexer.peek_token()whilenext_token.type==clex.LITERAL\
                andnext_token.valuein['*','/']:operator=self._parse_symbol()right=self.parse_exponentiation()left=BinaryNode(left,operator,right)next_token=self.lexer.peek_token()returnleft

where the use of lexer.peek_token() and the while loop show that the lexer class requires too much control from the upper level.

Back to parse_line(), it's clear that the code works, but it is not immediately easy to understand what the function does and when the old status is recovered. I'd really prefer something like

# PSEUDOCODE PSEUDOCODE PSEUDOCODE defparse_line(self):ATTEMPTreturnself.parse_assignment()returnself.parse_expression()

where I used a pseudo-keyword ATTEMPT to signal that somehow the lexer status is automatically stored at the beginning and retrieved at the end of it.

There's a very powerful concept in Python that allows us to write code like this, and it is called context manager. I won't go into the theory and syntax of context managers here, please refer to the Python documentation or your favourite course/book/website to discover how context managers work.

If I can add context manager features to the lexer the code of parse_line() might become

defparse_line(self):withself.lexer:returnself.parse_assignment()returnself.parse_expression()

Lexer

The first move is to transform the lexer into a context manager that does nothing. The test in tests/test_calc_lexer.py is

deftest_lexer_as_context_manager():l=clex.CalcLexer()l.load('abcd')withl:assertl.get_token()==token.Token(clex.NAME,'abcd')

When this works we have to be sure that the lexer does not restore the previous state outside the with statement if the code inside the statement ended without errors. The new test is

deftest_lexer_as_context_manager_does_not_restore_the_status_if_no_error():l=clex.CalcLexer()l.load('3 * 5')withl:assertl.get_token()==token.Token(clex.INTEGER,3)assertl.get_token()==token.Token(clex.LITERAL,'*')

Conversely, we need to be sure that the status is restored when the code inside the with statement fails, which is the whole point of the context manager. This is tested by

deftest_lexer_as_context_manager_restores_the_status_if_token_error():l=clex.CalcLexer()l.load('3 * 5')withl:l.get_token()l.get_token()raiseclex.TokenErrorassertl.get_token()==token.Token(clex.INTEGER,3)

When these three tests pass, we have a fully working context manager lexer, that reacts to TokenError exceptions going back to the previously stored status.

Parser

If the context manager lexer works as intended we should be able to replace the code of the parser without changing any test. The new code for parse_line() is the one I showed before

defparse_line(self):withself.lexer:returnself.parse_assignment()returnself.parse_expression()

and it works flawlessly.

The context manager part of the lexer, however, works if the code inside the with statement raises a TokenError exception when it fails. That exception is a signal to the context manager that the parsing path is not leading anywhere and it shall go back to the previous state.

Manage literals

The _parse_symbol() method is often used after some checks like

ifnext_token.type==clex.LITERALandnext_token.valuein['-','+']:operator=self._parse_symbol()

I would prefer to include the checks in the method itself, so that it might be included in a with statement. First of all the method can be renamed to _parse_literal(), and being an internal method I don't expect any test to fail

def_parse_literal(self):t=self.lexer.get_token()returnLiteralNode(t.value)

The method should also raise a TokenError when the token is not a LITERAL, and when the values are not the expected ones

def_parse_literal(self,values=None):t=self.lexer.get_token()ift.type!=clex.LITERAL:raiseclex.TokenErrorifvaluesandt.valuenotinvalues:raiseclex.TokenErrorreturnLiteralNode(t.value)

Note that using the default value for the values parameter I didn't change the current behaviour. The whole battery of tests still passes without errors.

Parsing factors

The next method that we can start changing is parse_factor(). The first pattern this function tries to parse is an unary node

ifnext_token.type==clex.LITERALandnext_token.valuein['-','+']:operator=self._parse_literal()factor=self.parse_factor()returnUnaryNode(operator,factor)

which may be converted to

withself.lexer:operator=self._parse_literal(['+','-'])content=self.parse_factor()returnUnaryNode(operator,content)

while still passing the whole test suite.

The second pattern are expressions surrounded by parenthesis

ifnext_token.type==clex.LITERALandnext_token.value=='(':self.lexer.discard_type(clex.LITERAL)expression=self.parse_expression()self.lexer.discard_type(clex.LITERAL)returnexpression

and this is easily converted to the new syntax as well

withself.lexer:self._parse_literal(['('])expression=self.parse_expression()self._parse_literal([')'])returnexpression

Parsing exponentiation operations

To change the parse_exponentiation() method we need first to make the _parse_variable() return a TokenError in case of wrong token

def_parse_variable(self):t=self.lexer.get_token()ift.type!=clex.NAME:raiseclex.TokenErrorreturnVariableNode(t.value)

then we can change the method we are interested in

defparse_exponentiation(self):left=self.parse_factor()withself.lexer:operator=self._parse_literal(['^'])right=self.parse_exponentiation()returnPowerNode(left,operator,right)returnleft

Doing this last substitution I also notice that there is some duplicated code in parse_factor(), and I replace it with a call to _parse_variable(). The test suite keeps passing, giving me the certainty that what I did does not change the behaviour of the code (at least the behaviour that is covered by tests).

Parsing terms

Now, the parse_term() method will be problematic. To implement this function I used a while loop that keeps using the parse_exponentiation() method until the separation token is a LITERAL with value * or /

defparse_term(self):left=self.parse_exponentiation()next_token=self.lexer.peek_token()whilenext_token.type==clex.LITERAL\
                andnext_token.valuein['*','/']:operator=self._parse_literal()right=self.parse_exponentiation()left=BinaryNode(left,operator,right)next_token=self.lexer.peek_token()returnleft

This is not a pure recursive call, then, and replacing the code with the context manager lexer would result in errors, because the context manage doesn't loop. The same situation is replicated in parse_expression().

This is another typical situation that we face when refactoring code. We realise that the required change is made of multiple steps and that multiple tests will fail until all the steps are completed.

There is no single solution to this problem, but TDD gives you some hints to deal with it. The most important "rule" that I follow when I work in a TDD environment is that there should be maximum one failing test at a time. And when a code change makes multiple tests fail there is a simple way to reach this situation: comment out tests.

Yes, you should temporarily get rid of tests, so you can concentrate in writing code that passes the subset of active tests. Then you will add one test at a time, fixing the code or the tests according to your needs. When you refactor it might be necessary to change the tests as well, as sometimes we test part of the code that are not exactly an external boundary.

I can now change the code of the parse_term() function introducing the context manager

defparse_term(self):left=self.parse_exponentiation()withself.lexer:operator=self._parse_literal(['*','/'])right=self.parse_exponentiation()returnBinaryNode(left,operator,right)returnleft

and the test suite runs with one single failing test, test_parse_term_with_multiple_operations(). I have now to work on it and try to understand why the test fails.

I decided to go for a pure recursive approach (no more while loops), which is what standard language parsers do. After working on it the new version of parse_term() is

defparse_term(self):left=self.parse_factor()withself.lexer:operator=self._parse_literal(['*','/'])right=self.parse_term()returnBinaryNode(left,operator,right)returnleft

And this change makes 3 tests fail. The same test_parse_term_with_multiple_operations() that was failing before, plus test_parse_exponentiation_with_other_operators(), and test_parse_exponentiation_with_parenthesis(). The last two actually test the method parse_exponentiation(), which uses parse_term(). This means that I can temporarily comment them and concentrate on the first one.

What I discover is that changing the code to use the recursive approach changes the output of the functions. The previous output of parse_term() applied to 2 * 3 / 4 was

{'type':'binary','left':{'type':'binary','left':{'type':'integer','value':2},'right':{'type':'integer','value':3},'operator':{'type':'literal','value':'*'}},'right':{'type':'integer','value':4},'operator':{'type':'literal','value':'/'}}

that is, the multiplication was stored first. Moving to a recursive approach makes the parse_term() function return this

{'type':'binary','left':{'type':'integer','value':2},'right':{'type':'binary','left':{'type':'integer','value':3},'right':{'type':'integer','value':4},'operator':{'type':'literal','value':'/'}},'operator':{'type':'literal','value':'*'}}

I think it is pretty clear that this structure, once visited, will return the same value as the previous one, as multiplication and division can be swapped. We will have to pay attention to this swap when operators with different priority are involved, like sum and multiplication, but for this test we can agree the result is no different.

This means that we may change the test and make it pass. Let me stress it once more: we have to understand why the test doesn't pass, and once we understood the reason, and decided it is acceptable, we can change the test.

Tests are not immutable, they are mere safeguards that raise alarms when you change the behaviour. It's up to you to deal with the alarm and to decide how what to do.

Once the test has been modified and the test suite passes, it's time to uncomment the first of the two remaining tests, test_parse_exponentiation_with_other_operators(). This test uses parse_term() to parse a string that contains an exponentiation, but the new code of the parse_term() method doesn't call the parse_exponentiation() function. So the test fails.

Parsing exponentiation

That tests tries to parse a string that contains a multiplication and an exponentiation, so the method that we should use to process it is parse_term(). The current version of parse_term(), however, doesn't consider exponentiation, so the new code is

defparse_term(self):left=self.parse_exponentiation()withself.lexer:operator=self._parse_literal(['*','/'])right=self.parse_term()returnBinaryNode(left,operator,right)returnleft

which makes all the active tests pass.

There is still one commented test, test_parse_exponentiation_with_parenthesis, that now passes with the new code.

Parsing expressions

The new version of parse_expression() has the same issue we found with parse_term(), that is the recursive approach changes the output.

defparse_expression(self):left=self.parse_term()withself.lexer:operator=self._parse_literal(['+','-'])right=self.parse_expression()left=BinaryNode(left,operator,right)returnleft

As before, we have to decide if the change is acceptable or if it is a real error. As happened for parse_term() the test can be safely changes to match the new code output.

Level 16 - Float numbers

So far, our calculator can handle only integer values, so it's time to add support for float numbers. This change shouldn't be complex: floating point numbers are easy to parse as they are basically two integer numbers separated by a dot.

Lexer

To test if the lexer understands floating point numbers it's enough to add this to tests/test_calc_lexer.py

deftest_get_tokens_understands_floats():l=clex.CalcLexer()l.load('3.6')assertl.get_tokens()==[token.Token(clex.FLOAT,'3.6'),token.Token(clex.EOL),token.Token(clex.EOF)]

Parser

To support float numbers it's enough to add that feature the method that we use to parse integers. We can rename parse_integer() to parse_number(), fix the test test_parse_integer(), and add test_parse_float()

deftest_parse_integer():p=cpar.CalcParser()p.lexer.load("5")node=p.parse_number()assertnode.asdict()=={'type':'integer','value':5}deftest_parse_float():p=cpar.CalcParser()p.lexer.load("5.8")node=p.parse_number()assertnode.asdict()=={'type':'float','value':5.8}

Visitor

The same thing that we did for the parser is valid for the visitor. We just need to copy the test for the integer numbers and adapt it

deftest_visitor_float():ast={'type':'float','value':12.345}v=cvis.CalcVisitor()assertv.visit(ast)==(12.345,'float')

This however leaves a bug in the visitor. The Test-Driven Development methodology can help you writing and changing your code, but cannot completely avoid bugs in the code. Actually, if you don't test cases, TDD can't do anything to find and remove bugs.

The bug I noticed after a while is that the visitor doesn't correctly manage an operation between integers and floats, returning an integer result. For example, if you sum 4 with 5.1 you should get 9.1 with type float. To test this behaviour we can add this code

deftest_visitor_expression_sum_with_float():ast={'type':'binary','left':{'type':'float','value':5.1},'right':{'type':'integer','value':4},'operator':{'type':'literal','value':'+'}}v=cvis.CalcVisitor()assertv.visit(ast)==(9.1,'float')

Conclusion

This post showed you how powerful the TDD methodology is when it comes to refactoring, or in general when the code has to be changed. Remember that tests can be changed if there are good reasons, and that the main point is to understand what's happening in your code and in the cases that you already tested.

Titles

The section quotes come from some good films: "Conan the Barbarian" (1982) and "Scrooged" (1988).

Feedback

Feel free to reach me on Twitter if you have questions. The GitHub issues page is the best place to submit corrections.

The Digital Cat: A game of tokens: solution - Part 4

$
0
0

This is my solution to the fourth part of "A Game of Tokens", which can be found here.

You can find the tests and the code presented here in this repository in the branch called part4.

Level 15 - Exponentiation

Lexer

The lexer can process the exponentiation operator ^ out of the box as a LITERAL token, so no changes to the code are needed.

Parser

The test test_parse_exponentiation() can be passed adding a PowerNode class

classPowerNode(BinaryNode):node_type='exponentiation'

and a parse_exponentiation() method to the parser

defparse_exponentiation(self):left=self.parse_factor()next_token=self.lexer.peek_token()ifnext_token.type==clex.LITERALandnext_token.value=='^':operator=self._parse_symbol()right=self.parse_exponentiation()returnPowerNode(left,operator,right)returnleft

This allows the parser to explicitly parse the exponentiation operation, but when the operation is mixed with others the parser doesn't know how to deal with it, as parse_exponentiation() is not called by any other method.

To pass the test_parse_exponentiation_with_other_operators() test we need to change the parse_term() method and call parse_exponentiation() instead of parse_factor()

defparse_term(self):left=self.parse_exponentiation()next_token=self.lexer.peek_token()whilenext_token.type==clex.LITERAL\
                andnext_token.valuein['*','/']:operator=self._parse_symbol()right=self.parse_exponentiation()left=BinaryNode(left,operator,right)

the full code of the CalcParser class is now

classCalcParser:def__init__(self):self.lexer=clex.CalcLexer()def_parse_symbol(self):t=self.lexer.get_token()returnLiteralNode(t.value)defparse_integer(self):t=self.lexer.get_token()returnIntegerNode(t.value)def_parse_variable(self):t=self.lexer.get_token()returnVariableNode(t.value)defparse_factor(self):next_token=self.lexer.peek_token()ifnext_token.type==clex.LITERALandnext_token.valuein['-','+']:operator=self._parse_symbol()factor=self.parse_factor()returnUnaryNode(operator,factor)ifnext_token.type==clex.LITERALandnext_token.value=='(':self.lexer.discard_type(clex.LITERAL)expression=self.parse_expression()self.lexer.discard_type(clex.LITERAL)returnexpressionifnext_token.type==clex.NAME:t=self.lexer.get_token()returnVariableNode(t.value)returnself.parse_integer()defparse_exponentiation(self):left=self.parse_factor()next_token=self.lexer.peek_token()ifnext_token.type==clex.LITERALandnext_token.value=='^':operator=self._parse_symbol()right=self.parse_exponentiation()returnPowerNode(left,operator,right)returnleftdefparse_term(self):left=self.parse_exponentiation()next_token=self.lexer.peek_token()whilenext_token.type==clex.LITERAL\
                andnext_token.valuein['*','/']:operator=self._parse_symbol()right=self.parse_exponentiation()left=BinaryNode(left,operator,right)next_token=self.lexer.peek_token()returnleftdefparse_expression(self):left=self.parse_term()next_token=self.lexer.peek_token()whilenext_token.type==clex.LITERAL\
                andnext_token.valuein['+','-']:operator=self._parse_symbol()right=self.parse_term()left=BinaryNode(left,operator,right)next_token=self.lexer.peek_token()returnleftdefparse_assignment(self):variable=self._parse_variable()self.lexer.discard(token.Token(clex.LITERAL,'='))value=self.parse_expression()returnAssignmentNode(variable,value)defparse_line(self):try:self.lexer.stash()returnself.parse_assignment()exceptclex.TokenError:self.lexer.pop()returnself.parse_expression()

Visitor

The given test test_visitor_exponentiation() requires the CalcVisitor to parse nodes of type exponentiation. The code required to do this is

ifnode['type']=='exponentiation':lvalue,ltype=self.visit(node['left'])rvalue,rtype=self.visit(node['right'])returnlvalue**rvalue,ltype

as a final case for the CalcVisitor class. The full code of the class is is now

classCalcVisitor:def__init__(self):self.variables={}defisvariable(self,name):returnnameinself.variablesdefvalueof(self,name):returnself.variables[name]['value']deftypeof(self,name):returnself.variables[name]['type']defvisit(self,node):ifnode['type']=='integer':returnnode['value'],node['type']ifnode['type']=='variable':returnself.valueof(node['value']),self.typeof(node['value'])ifnode['type']=='unary':operator=node['operator']['value']cvalue,ctype=self.visit(node['content'])ifoperator=='-':return-cvalue,ctypereturncvalue,ctypeifnode['type']=='binary':lvalue,ltype=self.visit(node['left'])rvalue,rtype=self.visit(node['right'])operator=node['operator']['value']ifoperator=='+':returnlvalue+rvalue,rtypeelifoperator=='-':returnlvalue-rvalue,rtypeelifoperator=='*':returnlvalue*rvalue,rtypeelifoperator=='/':returnlvalue//rvalue,rtypeifnode['type']=='assignment':right_value,right_type=self.visit(node['value'])self.variables[node['variable']]={'value':right_value,'type':right_type}returnNone,None

Level 16 - Float numbers

Lexer

The first thing the lexer need is a label to identify FLOAT tokens

FLOAT='FLOAT'

then the method _process_integer() cna be extended to process float numbers as well. To do this the method is renamed to _process_number(), the regular expression is modified, and the token_type is managed according to the presence of the dot.

def_process_number(self):regexp=re.compile('[\d\.]+')match=regexp.match(self._text_storage.tail)ifnotmatch:returnNonetoken_string=match.group()token_type=FLOATif'.'intoken_stringelseINTEGERreturnself._set_current_token_and_skip(token.Token(token_type,token_string))

Remember that the get_token() function has to be modified to use the new name of the method. The new code is

defget_token(self):eof=self._process_eof()ifeof:returneofeol=self._process_eol()ifeol:returneolself._process_whitespace()name=self._process_name()ifname:returnnameinteger=self._process_number()ifinteger:returnintegerliteral=self._process_literal()ifliteral:returnliteral

Parser

First we need to add a new type of node

classFloatNode(ValueNode):node_type='float'

The new version of parse_integer(), renamed parse_number(), shall deal with both cases but also raise the TokenError exception if the parsing fails

defparse_number(self):t=self.lexer.get_token()ift.type==clex.INTEGER:returnIntegerNode(int(t.value))elift.type==clex.FLOAT:returnFloatNode(float(t.value))raiseclex.TokenError

Visitor

The change to support float nodes is trivial, we just need to include it alongside with the integer case

defvisit(self,node):ifnode['type']in['integer','float']:returnnode['value'],node['type']

To fix the missing type promotion when dealing with integers and floats it's enough to add

ifltype=='float':rtype=ltype

just before evaluating the operator in the binary nodes. The full code of the visit() method is then

defvisit(self,node):ifnode['type']in['integer','float']:returnnode['value'],node['type']ifnode['type']=='variable':returnself.valueof(node['value']),self.typeof(node['value'])ifnode['type']=='unary':operator=node['operator']['value']cvalue,ctype=self.visit(node['content'])ifoperator=='-':return-cvalue,ctypereturncvalue,ctypeifnode['type']=='binary':lvalue,ltype=self.visit(node['left'])rvalue,rtype=self.visit(node['right'])operator=node['operator']['value']ifltype=='float':rtype=ltypeifoperator=='+':returnlvalue+rvalue,rtypeelifoperator=='-':returnlvalue-rvalue,rtypeelifoperator=='*':returnlvalue*rvalue,rtypeelifoperator=='/':returnlvalue//rvalue,rtypeifnode['type']=='assignment':right_value,right_type=self.visit(node['value'])self.variables[node['variable']]={'value':right_value,'type':right_type}returnNone,Noneifnode['type']=='exponentiation':lvalue,ltype=self.visit(node['left'])rvalue,rtype=self.visit(node['right'])returnlvalue**rvalue,ltype

Final words

I bet at this point of the challenge the addition of exponentiation and float numbers wasn't that hard. The refactoring might have been a bit thougher, however, but I hope that it showed you the real power of TDD.

Feedback

Feel free to reach me on Twitter if you have questions. The GitHub issues page is the best place to submit corrections.

PyCon: PyCon 2018 Code of Conduct Transparency Report

$
0
0
The PyCon Code of Conduct sets standards for how our community interacts with others during the conference. For 2018 the Code of Conduct underwent an extensive overhaul, our procedures for reporting and responding to incidents were improved, and our on-site methods were improved. You can read more about the updates for 2018 here.
Cumulatively these changes were meant to improve the safety, welcoming nature, and overall inclusivity of PyCon. Based on initial responses, feedback, and incidents reported this year we feel that we made progress on those goals.
A Code of Conduct without appropriate reporting and response procedures is difficult to enforce transparently, and furthermore a lack of transparency in the outcomes of Code of Conduct incidents leaves the community without knowledge of how or if the organizers worked to resolve incidents.
With that in mind, we have prepared the following to help the community understand what kind of incidents we received reports about and how the PyCon staff responded.

Overview

Reports

This year, PyCon Staff and Incident Responders were notified of 12 incidents. For summary purposes we categorized each report using a tenant of our standards and expectations of attendees.
10 were considered to be potential Code of Conduct matters:
  • Four related to unwelcome sexual attention or advances
  • Four related to other conduct that is inappropriate for a professional audience including people of many different backgrounds
  • One related to unwelcome physical contact
  • One related to sexist, racist, homophobic, transphobic, ableist, or exclusionary jokes
The two remaining reports were classified as Police Matters:
  • One regarding a missing persons report, we referred the reporter to the local police.
  • One regarding alleged attendee behavior at a local establishment unrelated to PyCon.

Resolutions

Of the 10 potential Code of Conduct matters:
  • Six were resolved on site
  • One was resolved post-conference
  • One is still undergoing resolution
  • Two could not be resolved as we did not have enough information to positively identify the person being reported.
The two unresolved incidents related to other conduct that is inappropriate for a professional audience including people of many different backgrounds:
  • A snide non-question question at a talk.
  • An incident of cultural appropriation.

Summary of Reported Incidents

  • An attendee was reported twice for unwanted sexual attention or advances. PyCon staff were able to identify the person but unable to contact them before the second incident. The reported attendee was removed from the conference, badge was forfeited, and they were instructed not to return. Conference center staff, PyCon Staff, and Incident Responders were directed to keep watch for the individual, who did not return.
  • An attendee made exclusionary statements and stated that the PyCon Code of Conduct excluded some attendees while eating breakfast with other attendees. PyCon Staff made contact with the reported attendee to discuss their concerns about the PyCon Code of Conduct and to reaffirm it's protection of all attendees. The reported person was told not to make exclusionary statements at PyCon and to keep a professional focus in further communication. No further follow up was taken.
  • An attendee was reported for their response to an incident of cultural appropriation. The reported attendee was wearing a traditional garb from another culture when another attendee approached them to discuss the meaning of the clothing. Later the reported attendee approached the other attendee in a confrontational manner, expecting them to further explain. PyCon Staff was unable to identify the individual for follow up.
  • An attendee asked a question during the question and answer period of a talk, the presenter's answer was then used to jeer the presenter for not using an existing tool. PyCon Staff was unable to identify the individual for follow up.
  • During the Poster Session, an attendee reported that another attendee was crowding a presenter and asking personal questions unrelated to their presentation. PyCon staff were able to discuss with the reported attendee. No further follow up was taken.
  • Attendees reported observing unwelcome sexual attention and advances by an attendee representing an employer towards another attendee who was looking for work. Follow up for this incident is ongoing.
  • Attendees reported a demo at a sponsor booth in the Expo Hall which was deemed in violation of PyCon's Code of Conduct. The sponsor immediately terminated the demo after PyCon Staff made contact. Representatives of the sponsor followed up with PyCon staff to make clear that the incident was understood and that the demo would no longer be used at PyCon or elsewhere. No further follow up was taken.
  • An attendee reported that another attendee had encroached on their personal space and exhibited unwanted attention at a PyCon event. PyCon staff followed up with the reported attendee. No further follow up was taken.
  • Volunteers reported a series of microaggressions another volunteer made at an evening gathering during the event. PyCon staff followed up with the volunteer and reinforced PyCon’s Code of Conduct, understanding was established and no further follow up on this incident was taken.

Summary of Police Matters

  • An attendee without a badge reported that they had not heard from another attendee in some time and that the attendees family and friends had been unable to contact them. Incident Responders referred the person to the local police and told them to call hospitals looking for John Does.
  • A detective from the local police reached out to Conference Staff asking for information on the whereabouts and contact information of an attendee based on a photograph. PyCon staff assisted to the best of their ability once the detective and report were verified for legitimacy.

    Vasudev Ram: Improved simple Python debugging function

    $
    0
    0
    By Vasudev Ram

    [ I rewrote parts of this post, which was originally published two days ago on my blog (but intentionally not to the Planet Python, earlier, because I did not set the python label that makes that happen), for more clarity and to give a standalone example of the use of the use of the debug1 function. ]

    I had blogged earlier about this Python debugging function, vr_debug, that I created a while ago:

    A simple Python debugging function

    Some time later I created an improved version of it, that does not need the user to set an environment variable to turn the debugging on or off.

    Here is the code for the new function, now called debug1, in module debug1:
    # debug1.py

    from __future__ import print_function

    # A simple debugging function for Python programs.
    # How to use it:
    # If the -O option is not given on the Python command line (the more common case
    # during development), the in-built special variable __debug__ is defined as True,
    # and the debug1 function displays debugging messages, i.e. it prints the message
    # and all other (optional) values passed to it.
    # If the -O option is given, the variable __debug__ is defined as False,
    # and the debug1 function does nothing is defined as a no-op, so does nothing.

    import os

    if __debug__:
    def debug1(message, *values):
    if len(values) == 0:
    print(message)
    else:
    print("{}:".format(message), end="")
    print("".join([repr(value) for value in values]))
    else:
    def debug1(message, *values):
    # Do nothing.
    pass

    def main():
    # Test the debug1 function with some calls.
    debug1('message only')
    debug1('message with int', 1)
    debug1('message with int, float', 1, 2.3)
    debug1('message with long, string', 4L, "hi")
    debug1('message with boolean, tuple, set', True, (1, 2), { 3, 4} )
    debug1('message with string, boolean, list', "hi", True, [2, 3])
    debug1('message with complex, dict', 1 + 2j, {'a': 'apple', 'b': 'banana'})
    class Foo: pass
    foo = Foo()
    debug1('message with object', foo)
    debug1('message with class', Foo)
    debug1('message with xrange', xrange(3))
    debug1('message with listiterator', iter(range(4)))

    if __name__ == '__main__':
    main()

    To use it in the normal way, import the debug1() function from the debug1 module into a Python file where you want to use it.
    Then just call the function in your code at the places where you want to print some message, with or without the value of one or more variables. Here is an example:


    # In your program, say factorial.py
    from debug1 import debug1

    def factorial(n):
    # This line prints the message and the value of n when debug1 is enabled.
    debug1("entered factorial, n", n)
    if n 0:
    raise Exception("Factorial argument must be integer, 0 or greater.")
    if n == 0:
    return 1
    p = 1
    for i in range(1, n + 1):
    p *= i
    # This line prints the message and the changing values
    # of i and p when debug1 is enabled.
    debug1("in for loop, i, p", i, p)
    return p

    print "i\tfactorial(i)"
    for i in range(6):
    print "{}\t{}".format(i, factorial(i))
    Then to run factorial.py with debugging on, no specific enabling step is needed (unlike with the earlier version, vr_debug.py where you had to set the environment variable VR_DEBUG to 1 or some other non-null value). Just run your program as usual and debugging output will be shown:
    $ python factorial.py
    i factorial(i)
    0 1
    in for loop, i, p: 1 1
    1 1
    in for loop, i, p: 1 1
    in for loop, i, p: 2 2
    2 2
    in for loop, i, p: 1 1
    in for loop, i, p: 2 2
    in for loop, i, p: 3 6
    3 6
    in for loop, i, p: 1 1
    in for loop, i, p: 2 2
    in for loop, i, p: 3 6
    in for loop, i, p: 4 24
    4 24
    in for loop, i, p: 1 1
    in for loop, i, p: 2 2
    in for loop, i, p: 3 6
    in for loop, i, p: 4 24
    in for loop, i, p: 5 120
    5 120
    Once you have debugged and fixed any bugs in your program, with the help of the debugging output, you can easily turn off debugging messages like this, by adding the -O option to the python command line, to get only the normal program output:
    $ python -O factorial.py
    i factorial(i)
    0 1
    1 1
    2 2
    3 6
    4 24
    5 120
    The debug1 module internally checks the value of the built-in Python variable __debug__, and conditionally defines the function debug1() as either the real function, or a no-op, based on __debug__'s value at runtime.

    The __debug__ variable is normally set by the Python interpreter to True, unless you pass python the -O option, which sets it to False.

    Know of any different or better debugging functions? Feel free to mention them in the comments. Like I said in the previous post about the first version of this debug function (linked above), I've never been quite satisfied with the various attempts I've made to write debugging functions of this kind.

    Of course, Python IDEs like Wing IDE or PyCharm can be used, which have features like stepping through (or over, in the case of functions) the code, setting breakpoints and watches, etc., but sometimes the good old debugging print statement technique is more suitable, particularly when there are many iterations of a loop, in which case the breakpoint / watch method becomes tedious, unless conditional breakpoints or suchlike are supported.

    There are also scenarios where IDE debugging does not work well or is not supported, like in the case of web development. Although some IDEs have made attempts in this direction, sometimes it is only available in a paid or higher version.


    Interested in learning Python programming by email? Contact me for the course details (use the Gmail id at that preceding link).
    Enjoy.

    - Vasudev Ram - Online Python training and consulting

    Get updates (via Gumroad) on my forthcoming apps and content.

    Jump to posts: Python * DLang * xtopdf

    Subscribe to my blog by email

    My ActiveState Code recipes

    Follow me on: LinkedIn * Twitter

    Are you a blogger with some traffic? Get Convertkit:

    Email marketing for professional bloggers


    Yasoob Khalid: Top 14 MOST famous Python libraries & frameworks

    $
    0
    0

    Hi everyone! I recently decided to step into YouTube video making. This is my first video about 14 of my most favourite and most famous Python libraries and frameworks. Please take a look and if you have ANY suggestions as to how I can improve the quality of the videos in the future please let me know in the comments below.  

     

    Libraries covered:

    1. requests
    2. tqdm
    3. pillow
    4. scrapy
    5. numpy
    6. pandas
    7. scapy
    8. matplotlib
    9. kivy
    10. nltk
    11. keras
    12. SQLAlchemy
    13. Django
    14. Twisted

    Extra libraries:

    1. flask
    2. scipy
    3. pytorch
    4. DjangoCMS
    5. click
    6. astropy
    7. scikit-learn
    8. opencv
    9. PyQt
    10. tensorflow
    11. wagtail

    Till next time, take care!

    The Digital Cat: Clean architectures in Python: a step-by-step example

    $
    0
    0

    In 2015 I was introduced by my friend Roberto Ciatti to the concept of Clean Architecture, as it is called by Robert Martin. The well-known Uncle Bob talks a lot about this concept at conferences and wrote some very interesting posts about it. What he calls "Clean Architecture" is a way of structuring a software system, a set of consideration (more than strict rules) about the different layers and the role of the actors in it.

    As he clearly states in a post aptly titled The Clean Architecture, the idea behind this design is not new, being built on a set of concepts that have been pushed by many software engineers over the last 3 decades. One of the first implementations may be found in the Boundary-Control-Entity model proposed by Ivar Jacobson in his masterpiece "Object-Oriented Software Engineering: A Use Case Driven Approach" published in 1992, but Martin lists other more recent versions of this architecture.

    I will not repeat here what he had already explained better than I can do, so I will just point out some resources you may check to start exploring these concepts:

    The purpose of this post is to show how to build a web service in Python from scratch using a clean architecture. One of the main advantages of this layered design is testability, so I will develop it following a TDD approach. The project was initially developed from scratch in around 3 hours of work. Given the toy nature of the project some choices have been made to simplify the resulting code. Whenever meaningful I will point out those simplifications and discuss them.

    If you want to know more about TDD in Python read the posts in this category.

    Project overview

    The goal of the "Rent-o-matic" project (fans of Day of the Tentacle may get the reference) is to create a simple search engine on top of a dataset of objects which are described by some quantities. The search engine shall allow to set some filters to narrow the search.

    The objects in the dataset are storage rooms for rent described by the following quantities:

    • An unique identifier
    • A size in square meters
    • A renting price in Euro/day
    • Latitude and longitude

    As pushed by the clean architecture model, we are interested in separating the different layers of the system. The architecture is described by four layers, which however can be implemented by more than four actual code modules. I will give here a brief description of those layers.

    Entities

    This is the level in which the domain models are described. Since we work in Python, I will put here the class that represent my storage rooms, with the data contained in the database, and whichever data I think is useful to perform the core business processing.

    It is very important to understand that the models in this layer are different from the usual models of framework like Django. These models are not connected with a storage system, so they cannot be directly saved or queried using methods of their classes. They may however contain helper methods that implement code related to the business rules.

    Use cases

    This layer contains the use cases implemented by the system. In this simple example there will be only one use case, which is the list of storage rooms according to the given filters. Here you would put for example a use case that shows the detail of a given storage room or every business process you want to implement, such as booking a storage room, filling it with goods, etc.

    Interface Adapters

    This layer corresponds to the boundary between the business logic and external systems and implements the APIs used to exchange data with them. Both the storage system and the user interface are external systems that need to exchange data with the use cases and this layer shall provide an interface for this data flow. In this project the presentation part of this layer is provided by a JSON serializer, on top of which an external web service may be built. The storage adapter shall define here the common API of the storage systems.

    External interfaces

    This part of the architecture is made by external systems that implement the interfaces defined in the previous layer. Here for example you will find a web server that implements (REST) entry points, which access the data provided by use cases through the JSON serializer. You will also find here the storage system implementation, for example a given database such as MongoDB.

    API and shades of grey

    The word API is of uttermost importance in a clean architecture. Every layer may be accessed by an API, that is a fixed collection of entry points (methods or objects). Here "fixed" means "the same among every implementation", obviously an API may change with time. Every presentation tool, for example, will access the same use cases, and the same methods, to obtain a set of domain models, which are the output of that particular use case. It is up to the presentation layer to format data according to the specific presentation media, for example HTML, PDF, images, etc. If you understand plugin-based architectures you already grasped the main concept of a separate, API-driven component (or layer).

    The same concept is valid for the storage layer. Every storage implementation shall provide the same methods. When dealing with use cases you shall not be concerned with the actual system that stores data, it may be a MongoDB local installation, a cloud storage system or a trivial in-memory dictionary.

    The separation between layers, and the content of each layer, is not always fixed and immutable. A well-designed system shall also cope with practical world issues such as performances, for example, or other specific needs. When designing an architecture it is very important to know "what is where and why", and this is even more important when you "bend" the rules. Many issues do not have a black-or-white answer, and many decisions are "shades of grey", that is it is up to you to justify why you put something in a given place.

    Keep in mind however, that you should not break the structure of the clean architecture, in particular you shall be inflexible about the data flow (see the "Crossing boundaries" section in the original post of Robert Martin). If you break the data flow, you are basically invalidating the whole structure. Let me stress it again: never break the data flow. A simple example of breaking the data flow is to let a use case output a Python class instead of a representation of that class such as a JSON string.

    Project structure

    Let us take a look at the final structure of the project

    The global structure of the package has been built with Cookiecutter, and I will run quickly through that part. The rentomatic directory contains the following subdirectories: domain, repositories, REST, serializers, use_cases. Those directories reflect the layered structure introduced in the previous section, and the structure of the tests directory mirrors this structure so that tests are easily found.

    Source code

    You can find the source code in this GitHub repository. Feel free to fork it and experiment, change it, and find better solutions to the problem I will discuss in this post. The source code contains tagged commits to allow you to follow the actual development as presented in the post. You can find the current tag in the Git tag: <tag name> label under the section titles. The label is actually a link to the tagged commit on GitHub, if you want to see the code without cloning it.

    Project initialization

    Git tag: step01

    Update: this Cookiecutter package creates an environment like the one I am creating in this section. I will keep the following explanation so that you can see how to manage requirements and configurations, but for your next project consider using this automated tool.

    I usually like maintaining a Python virtual environment inside the project, so I will create a temporary virtualenv to install cookiecutter, create the project, and remove the virtualenv. Cookiecutter is going to ask you some questions about you and the project, to provide an initial file structure. We are going to build our own testing environment, so it is safe to answer no to use_pytest. Since this is a demo project we are not going to need any publishing feature, so you can answer no to use_pypi_deployment_with_travis as well. The project does not have a command line interface, and you can safely create the author file and use any license.

    virtualenv venv3 -p python3
    source venv3/bin/activate
    pip install cookiecutter
    cookiecutter https://github.com/audreyr/cookiecutter-pypackage
    

    Now answer the questions, then finish creating the project with the following code

    deactivate
    rm -fR venv3
    cd rentomatic
    virtualenv venv3 -p python3
    source venv3/bin/activate
    

    Get rid of the requirements_dev.txt file that Cookiecutter created for you. I usually store virtualenv requirements in different hierarchical files to separate production, development and testing environments, so create the requirements directory and the relative files

    mkdir requirements
    touch requirements/prod.txt
    touch requirements/dev.txt
    touch requirements/test.txt
    

    The test.txt file will contain specific packages used to test the project. Since to test the project you also need to install the packages for the production environment the file will first include the production one.

    -r prod.txt
    
    pytest
    tox
    coverage
    pytest-cov
    

    The dev.txt file will contain packages used during the development process and shall install also test and production package

    -r test.txt
    
    pip
    wheel
    flake8
    Sphinx
    

    (taking advantage of the fact that test.txt already includes prod.txt).

    Last, the main requirements.txt file of the project will just import requirements/prod.txt

    -r prod.txt
    

    Obviously you are free to find the project structure that better suits your need or preferences. This is the structure we are going to use in this project but nothing forces you to follow it in your personal projects.

    This separation allows you to install a full-fledged development environment on your machine, while installing only testing tools in a testing environment like the Travis platform and to further reduce the amount of dependencies in the production case.

    As you can see, I am not using version tags in the requirements files. This is because this project is not going to be run in a production environment, so we do not need to freeze the environment.

    Remember at this point to install the development requirements in your virtualenv

    $ pip install -r requirements/dev.txt
    

    Miscellaneous configuration

    The pytest testing library needs to be configured. This is the pytest.ini file that you can create in the root directory (where the setup.py file is located)

    [pytest]
    minversion = 2.0
    norecursedirs = .git .tox venv* requirements*
    python_files = test*.py
    

    To run the tests during the development of the project just execute

    $ py.test -sv
    

    If you want to check the coverage, i.e. the amount of code which is run by your tests or "covered", execute

    $ py.test --cov-report term-missing --cov=rentomatic
    

    If you want to know more about test coverage check the official documentation of the Coverage.py and the pytest-cov packages.

    I strongly suggest the use of the flake8 package to check that your Python code is PEP8 compliant. This is the flake8 configuration that you can put in your setup.cfg file

    [flake8]
    ignore = D203
    exclude = .git, venv*, docs
    max-complexity = 10
    

    To check the compliance of your code with the PEP8 standard execute

    $ flake8
    

    Flake8 documentation is available here.

    Note that every step in this post produces tested code and a of coverage of 100%. One of the benefits of a clean architecture is the separation between layers, and this guarantees a great degree of testability. Note however that in this tutorial, in particular in the REST sections, some tests have been omitted in favour of a simpler description of the architecture.

    Domain models

    Git tag: step02

    Let us start with a simple definition of the StorageRoom model. As said before, the clean architecture models are very lightweight, or at least they are lighter than their counterparts in a framework.

    Following the TDD methodology the first thing that I write are the tests. Create the tests/domain/test_storageroom.py and put this code inside it

    importuuidfromrentomatic.domainimportmodelsasmdeftest_storageroom_model_init():code=uuid.uuid4()storageroom=m.StorageRoom(code,size=200,price=10,longitude='-0.09998975',latitude='51.75436293')assertstorageroom.code==codeassertstorageroom.size==200assertstorageroom.price==10assertstorageroom.longitude==-0.09998975assertstorageroom.latitude==51.75436293deftest_storageroom_model_from_dict():code=uuid.uuid4()storageroom=m.StorageRoom.from_dict({'code':code,'size':200,'price':10,'longitude':'-0.09998975','latitude':'51.75436293'})assertstorageroom.code==codeassertstorageroom.size==200assertstorageroom.price==10assertstorageroom.longitude==-0.09998975assertstorageroom.latitude==51.75436293

    With these two tests we ensure that our model can be initialized with the correct values and that can be created from a dictionary. In this first version all the parameters of the model are required. Later we could want to make some of them optional, and in that case we will have to add the relevant tests.

    Now let's write the StorageRoom class in the rentomatic/domain/storageroom.py file. Do not forget to create the __init__.py file in the subdirectories of the project, otherwise Python will not be able to import the modules.

    fromrentomatic.shared.domain_modelimportDomainModelclassStorageRoom(object):def__init__(self,code,size,price,latitude,longitude):self.code=codeself.size=sizeself.price=priceself.latitude=float(latitude)self.longitude=float(longitude)@classmethoddeffrom_dict(cls,adict):room=StorageRoom(code=adict['code'],size=adict['size'],price=adict['price'],latitude=adict['latitude'],longitude=adict['longitude'],)returnroomDomainModel.register(StorageRoom)

    The model is very simple, and requires no further explanation. One of the benefits of a clean architecture is that each layer contains small pieces of code that, being isolated, shall perform simple tasks. In this case the model provides an initialization API and stores the information inside the class.

    The from_dict method comes in handy when we have to create a model from data coming from another layer (such as the database layer or the query string of the REST layer).

    One could be tempted to try to simplify the from_dict function, abstracting it and providing it through a Model class. Given that a certain level of abstraction and generalization is possible and desirable, the initialization part of the models shall probably deal with various different cases, and thus is better off being implemented directly in the class.

    The DomainModel abstract base class is an easy way to categorize the model for future uses like checking if a class is a model in the system. For more information about this use of Abstract Base Classes in Python see this post.

    Serializers

    Git tag: step03

    Our model needs to be serialized if we want to return it as a result of an API call. The typical serialization format is JSON, as this is a broadly accepted standard for web-based API. The serializer is not part of the model, but is an external specialized class that receives the model instance and produces a representation of its structure and values.

    To test the JSON serialization of our StorageRoom class put in the tests/serializers/test_storageroom_serializer.py file the following code

    importjsonfromrentomatic.serializersimportstorageroom_serializerassrsfromrentomatic.domainimportmodelsdeftest_serialize_domain_storageroom():room=models.StorageRoom('f853578c-fc0f-4e65-81b8-566c5dffa35a',size=200,price=10,longitude='-0.09998975',latitude='51.75436293')expected_json="""        {"code": "f853578c-fc0f-4e65-81b8-566c5dffa35a","size": 200,"price": 10,"longitude": -0.09998975,"latitude": 51.75436293        }"""assertjson.loads(json.dumps(room,cls=srs.StorageRoomEncoder))==json.loads(expected_json)

    Put in the rentomatic/serializers/storageroom_serializer.py file the code that makes the test pass

    importjsonclassStorageRoomEncoder(json.JSONEncoder):defdefault(self,o):try:to_serialize={'code':o.code,'size':o.size,'price':o.price,"latitude":o.latitude,"longitude":o.longitude,}returnto_serializeexceptAttributeError:returnsuper().default(o)

    Providing a class that inherits from json.JSONEncoder let us use the json.dumps(room, cls=StorageRoomEncoder) syntax to serialize the model.

    There is a certain degree of repetition in the code we wrote, and this is the annoying part of a clean architecture. Since we want to isolate layers as much as possible and create lightweight classes we end up somehow repeating certain types of actions. For example the serialization code that assigns attributes of a StorageRoom to JSON attributes is very similar to that we use to create the object from a dictionary. Not exactly the same, obviously, but the two functions are very close.

    Use cases (part 1)

    Git tag: step04

    It's time to implement the actual business logic our application wants to expose to the outside world. Use cases are the place where we implement classes that query the repository, apply business rules, logic, and whatever transformation we need for our data, and return the results.

    With those requirements in mind, let us start to build a use case step by step. The simplest use case we can create is one that fetches all the storage rooms from the repository and returns them. Please note that we did not implement any repository layer yet, so our tests will mock it.

    This is the skeleton for a basic test of a use case that lists all the storage rooms. Put this code in the tests/use_cases/test_storageroom_list_use_case.py

    importpytestfromunittestimportmockfromrentomatic.domainimportmodelsfromrentomatic.use_casesimportstorageroom_use_casesasuc@pytest.fixturedefdomain_storagerooms():storageroom_1=models.StorageRoom(code='f853578c-fc0f-4e65-81b8-566c5dffa35a',size=215,price=39,longitude='-0.09998975',latitude='51.75436293',)storageroom_2=models.StorageRoom(code='fe2c3195-aeff-487a-a08f-e0bdc0ec6e9a',size=405,price=66,longitude='0.18228006',latitude='51.74640997',)storageroom_3=models.StorageRoom(code='913694c6-435a-4366-ba0d-da5334a611b2',size=56,price=60,longitude='0.27891577',latitude='51.45994069',)storageroom_4=models.StorageRoom(code='eed76e77-55c1-41ce-985d-ca49bf6c0585',size=93,price=48,longitude='0.33894476',latitude='51.39916678',)return[storageroom_1,storageroom_2,storageroom_3,storageroom_4]deftest_storageroom_list_without_parameters(domain_storagerooms):repo=mock.Mock()repo.list.return_value=domain_storageroomsstorageroom_list_use_case=uc.StorageRoomListUseCase(repo)result=storageroom_list_use_case.execute()repo.list.assert_called_with()assertresult==domain_storagerooms

    The test is straightforward. First we mock the repository so that is provides a list() method that returns the list of models we created above the test. Then we initialize the use case with the repo and execute it, collecting the result. The first thing we check is if the repository method was called without any parameter, and the second is the effective correctness of the result.

    This is the implementation of the use case that makes the test pass. Put the code in the rentomatic/use_cases/storageroom_use_case.py

    classStorageRoomListUseCase(object):def__init__(self,repo):self.repo=repodefexecute(self):returnself.repo.list()

    With such an implementation of the use case, however, we will soon experience issues. For starters, we do not have a standard way to transport the call parameters, which means that we do not have a standard way to check for their correctness either. The second problem is that we miss a standard way to return the call results and consequently we lack a way to communicate if the call was successful of if it failed, and in the latter case what are the reasons of the failure. This applies also to the case of bad parameters discussed in the previous point.

    We want thus to introduce some structures to wrap input and outputs of our use cases. Those structures are called request and response objects.

    Requests and responses

    Git tag: step05

    Request and response objects are an important part of a clean architecture, as they transport call parameters, inputs and results from outside the application into the use cases layer.

    More specifically, requests are objects created from incoming API calls, thus they shall deal with things like incorrect values, missing parameters, wrong formats, etc. Responses, on the other hand, have to contain the actual results of the API calls, but shall also be able to represent error cases and to deliver rich information on what happened.

    The actual implementation of request and response objects is completely free, the clean architecture says nothing about them. The decision on how to pack and represent data is up to us.

    For the moment we just need a StorageRoomListRequestObject that can be initialized without parameters, so let us create the file tests/use_cases/test_storageroom_list_request_objects.py and put there a test for this object.

    fromrentomatic.use_casesimportrequest_objectsasrodeftest_build_storageroom_list_request_object_without_parameters():req=ro.StorageRoomListRequestObject()assertbool(req)isTruedeftest_build_file_list_request_object_from_empty_dict():req=ro.StorageRoomListRequestObject.from_dict({})assertbool(req)isTrue

    While at the moment this request object is basically empty, it will come in handy as soon as we start having parameters for the list use case. The code of the StorageRoomListRequestObject is the following and goes into the rentomatic/use_cases/request_objects.py file

    classStorageRoomListRequestObject(object):@classmethoddeffrom_dict(cls,adict):returnStorageRoomListRequestObject()def__nonzero__(self):returnTrue

    The response object is also very simple, since for the moment we just need a successful response. Unlike the request, the response is not linked to any particular use case, so the test file can be named tests/shared/test_response_object.py

    fromrentomatic.sharedimportresponse_objectasrodeftest_response_success_is_true():assertbool(ro.ResponseSuccess())isTrue

    and the actual response object is in the file rentomatic/shared/response_object.py

    classResponseSuccess(object):def__init__(self,value=None):self.value=valuedef__nonzero__(self):returnTrue__bool__=__nonzero__

    Use cases (part 2)

    Git tag: step06

    Now that we have implemented the request and response object we can change the test code to include those structures. Change the tests/use_cases/test_storageroom_list_use_case.py to contain this code

    importpytestfromunittestimportmockfromrentomatic.domainimportmodelsfromrentomatic.use_casesimportrequest_objectsasrofromrentomatic.use_casesimportstorageroom_use_casesasuc@pytest.fixturedefdomain_storagerooms():storageroom_1=models.StorageRoom(code='f853578c-fc0f-4e65-81b8-566c5dffa35a',size=215,price=39,longitude='-0.09998975',latitude='51.75436293',)storageroom_2=models.StorageRoom(code='fe2c3195-aeff-487a-a08f-e0bdc0ec6e9a',size=405,price=66,longitude='0.18228006',latitude='51.74640997',)storageroom_3=models.StorageRoom(code='913694c6-435a-4366-ba0d-da5334a611b2',size=56,price=60,longitude='0.27891577',latitude='51.45994069',)storageroom_4=models.StorageRoom(code='eed76e77-55c1-41ce-985d-ca49bf6c0585',size=93,price=48,longitude='0.33894476',latitude='51.39916678',)return[storageroom_1,storageroom_2,storageroom_3,storageroom_4]deftest_storageroom_list_without_parameters(domain_storagerooms):repo=mock.Mock()repo.list.return_value=domain_storageroomsstorageroom_list_use_case=uc.StorageRoomListUseCase(repo)request_object=ro.StorageRoomListRequestObject.from_dict({})response_object=storageroom_list_use_case.execute(request_object)assertbool(response_object)isTruerepo.list.assert_called_with()assertresponse_object.value==domain_storagerooms

    The new version of the rentomatic/use_case/storageroom_use_cases.py file is the following

    fromrentomatic.sharedimportresponse_objectasroclassStorageRoomListUseCase(object):def__init__(self,repo):self.repo=repodefexecute(self,request_object):storage_rooms=self.repo.list()returnro.ResponseSuccess(storage_rooms)

    Let us consider what we have achieved with our clean architecture up to this point. We have a very lightweight model that can be serialized to JSON and which is completely independent from other parts of the system. The code also contains a use case that, given a repository that exposes a given API, extracts all the models and returns them contained in a structured object.

    We are missing some objects, however. For example, we have not implemented any unsuccessful response object or validated the incoming request object.

    To explore these missing parts of the architecture let us improve the current use case to accept a filters parameter that represents some filters that we want to apply to the extracted list of models. This will generate some possible error conditions for the input, forcing us to introduce some validation for the incoming request object.

    Requests and validation

    Git tag: step07

    I want to add a filters parameter to the request. Through that parameter the caller can add different filters by specifying a name and a value for each filter (for instance {'price_lt': 100} to get all results with a price lesser than 100).

    The first thing to do is to change the request object, starting from the test. The new version of the tests/use_cases/test_storageroom_list_request_objects.py file is the following

    fromrentomatic.use_casesimportrequest_objectsasrodeftest_build_storageroom_list_request_object_without_parameters():req=ro.StorageRoomListRequestObject()assertreq.filtersisNoneassertbool(req)isTruedeftest_build_file_list_request_object_from_empty_dict():req=ro.StorageRoomListRequestObject.from_dict({})assertreq.filtersisNoneassertbool(req)isTruedeftest_build_storageroom_list_request_object_with_empty_filters():req=ro.StorageRoomListRequestObject(filters={})assertreq.filters=={}assertbool(req)isTruedeftest_build_storageroom_list_request_object_from_dict_with_empty_filters():req=ro.StorageRoomListRequestObject.from_dict({'filters':{}})assertreq.filters=={}assertbool(req)isTruedeftest_build_storageroom_list_request_object_with_filters():req=ro.StorageRoomListRequestObject(filters={'a':1,'b':2})assertreq.filters=={'a':1,'b':2}assertbool(req)isTruedeftest_build_storageroom_list_request_object_from_dict_with_filters():req=ro.StorageRoomListRequestObject.from_dict({'filters':{'a':1,'b':2}})assertreq.filters=={'a':1,'b':2}assertbool(req)isTruedeftest_build_storageroom_list_request_object_from_dict_with_invalid_filters():req=ro.StorageRoomListRequestObject.from_dict({'filters':5})assertreq.has_errors()assertreq.errors[0]['parameter']=='filters'assertbool(req)isFalse

    As you can see I added the assert req.filters is None check to the original two tests, then I added 5 tests to check if filters can be specified and to test the behaviour of the object with an invalid filter parameter.

    To make the tests pass we have to change our StorageRoomListRequestObject class. There are obviously multiple possible solutions that you can come up with, and I recommend you to try to find your own. This is the one I usually employ. The file rentomatic/use_cases/request_object.py becomes

    importcollectionsclassInvalidRequestObject(object):def__init__(self):self.errors=[]defadd_error(self,parameter,message):self.errors.append({'parameter':parameter,'message':message})defhas_errors(self):returnlen(self.errors)>0def__nonzero__(self):returnFalse__bool__=__nonzero__classValidRequestObject(object):@classmethoddeffrom_dict(cls,adict):raiseNotImplementedErrordef__nonzero__(self):returnTrue__bool__=__nonzero__classStorageRoomListRequestObject(ValidRequestObject):def__init__(self,filters=None):self.filters=filters@classmethoddeffrom_dict(cls,adict):invalid_req=InvalidRequestObject()if'filters'inadictandnotisinstance(adict['filters'],collections.Mapping):invalid_req.add_error('filters','Is not iterable')ifinvalid_req.has_errors():returninvalid_reqreturnStorageRoomListRequestObject(filters=adict.get('filters',None))

    Let me review this new code bit by bit.

    First of all, two helper objects have been introduced, ValidRequestObject and InvalidRequestObject. They are different because an invalid request shall contain the validation errors, but both can be converted to booleans.

    Second, the StorageRoomListRequestObject accepts an optional filters parameter when instantiated. There are no validation checks in the __init__() method because this is considered to be an internal method that gets called when the parameters have already been validated.

    Last, the from_dict() method performs the validation of the filters parameter, if it is present. I leverage the collections.Mapping abstract base class to check if the incoming parameter is a dictionary-like object and return either an InvalidRequestObject or a ValidRequestObject instance.

    Since we can now tell bad requests from good ones we need to introduce a new type of response as well, to manage bad requests or other errors in the use case.

    Responses and failures

    Git tag: step08

    What happens if the use case encounter an error? Use cases can encounter a wide set of errors: validation errors, as we just discussed in the previous section, but also business logic errors or errors that come from the repository layer. Whatever the error, the use case shall always return an object with a known structure (the response), so we need a new object that provides a good support for different types of failures.

    As happened for the requests there is no unique way to provide such an object, and the following code is just one of the possible solutions.

    The first thing to do is to expand the tests/shared/test_response_object.py file, adding tests for failures.

    importpytestfromrentomatic.sharedimportresponse_objectasresfromrentomatic.use_casesimportrequest_objectsasreq@pytest.fixturedefresponse_value():return{'key':['value1','value2']}@pytest.fixturedefresponse_type():return'ResponseError'@pytest.fixturedefresponse_message():return'This is a response error'

    This is some boilerplate code, basically pytest fixtures that we will use in the following tests.

    deftest_response_success_is_true(response_value):assertbool(res.ResponseSuccess(response_value))isTruedeftest_response_failure_is_false(response_type,response_message):assertbool(res.ResponseFailure(response_type,response_message))isFalse

    Two basic tests to check that both the old ResponseSuccess and the new ResponseFailure objects behave consistently when converted to boolean.

    deftest_response_success_contains_value(response_value):response=res.ResponseSuccess(response_value)assertresponse.value==response_value

    The ResponseSuccess object contains the call result in the value attribute.

    deftest_response_failure_has_type_and_message(response_type,response_message):response=res.ResponseFailure(response_type,response_message)assertresponse.type==response_typeassertresponse.message==response_messagedeftest_response_failure_contains_value(response_type,response_message):response=res.ResponseFailure(response_type,response_message)assertresponse.value=={'type':response_type,'message':response_message}

    These two tests ensure that the ResponseFailure object provides the same interface provided by the successful one and that the type and message parameter are accessible.

    deftest_response_failure_initialization_with_exception():response=res.ResponseFailure(response_type,Exception('Just an error message'))assertbool(response)isFalseassertresponse.type==response_typeassertresponse.message=="Exception: Just an error message"deftest_response_failure_from_invalid_request_object():response=res.ResponseFailure.build_from_invalid_request_object(req.InvalidRequestObject())assertbool(response)isFalsedeftest_response_failure_from_invalid_request_object_with_errors():request_object=req.InvalidRequestObject()request_object.add_error('path','Is mandatory')request_object.add_error('path',"can't be blank")response=res.ResponseFailure.build_from_invalid_request_object(request_object)assertbool(response)isFalseassertresponse.type==res.ResponseFailure.PARAMETERS_ERRORassertresponse.message=="path: Is mandatory\npath: can't be blank"

    We sometimes want to create responses from Python exceptions that can happen in the use case, so we test that ResponseFailure objects can be initialized with a generic exception.

    And last we have the tests for the build_from_invalid_request_object() method that automate the initialization of the response from an invalid request. If the request contains errors (remember that the request validates itself), we need to put them into the response message.

    The last test uses a class attribute to classify the error. The ResponseFailure class will contain three predefined errors that can happen when running the use case, namely RESOURCE_ERROR, PARAMETERS_ERROR, and SYSTEM_ERROR. This categorization is an attempt to capture the different types of issues that can happen when dealing with an external system through an API. RESOURCE_ERROR contains all those errors that are related to the resources contained in the repository, for instance when you cannot find an entry given its unique id. PARAMETERS_ERROR describes all those errors that occur when the request parameters are wrong or missing. SYSTEM_ERROR encompass the errors that happen in the underlying system at operating system level, such as a failure in a filesystem operation, or a network connection error while fetching data from the database.

    The use case has the responsibility to manage the different error conditions arising from the Python code and to convert them into an error description made of one of the three types I just described and a message.

    Let's write the ResponseFailure class that makes the tests pass. This can be the initial definition of the class. Put it in rentomatic/shared/response_object.py

    classResponseFailure(object):RESOURCE_ERROR='ResourceError'PARAMETERS_ERROR='ParametersError'SYSTEM_ERROR='SystemError'def__init__(self,type_,message):self.type=type_self.message=self._format_message(message)def_format_message(self,msg):ifisinstance(msg,Exception):return"{}: {}".format(msg.__class__.__name__,"{}".format(msg))returnmsg

    Through the _format_message() method we enable the class to accept both string messages and Python exceptions, which is very handy when dealing with external libraries that can raise exceptions we do not know or do not want to manage.

    @propertydefvalue(self):return{'type':self.type,'message':self.message}

    This property makes the class comply with the ResponseSuccess API, providing the value attribute, which is an aptly formatted dictionary.

    def__nonzero__(self):returnFalse__bool__=__nonzero__@classmethoddefbuild_from_invalid_request_object(cls,invalid_request_object):message="\n".join(["{}: {}".format(err['parameter'],err['message'])forerrininvalid_request_object.errors])returncls(cls.PARAMETERS_ERROR,message)

    As explained before, the PARAMETERS_ERROR type encompasses all those errors that come from an invalid set of parameters, which is the case of this function, that shall be called whenever the request is wrong, which means that some parameters contain errors or are missing.

    Since building failure responses is a common activity it is useful to have helper methods, so I add three tests for the building functions to the tests/shared/test_response_object.py file

    deftest_response_failure_build_resource_error():response=res.ResponseFailure.build_resource_error("test message")assertbool(response)isFalseassertresponse.type==res.ResponseFailure.RESOURCE_ERRORassertresponse.message=="test message"deftest_response_failure_build_parameters_error():response=res.ResponseFailure.build_parameters_error("test message")assertbool(response)isFalseassertresponse.type==res.ResponseFailure.PARAMETERS_ERRORassertresponse.message=="test message"deftest_response_failure_build_system_error():response=res.ResponseFailure.build_system_error("test message")assertbool(response)isFalseassertresponse.type==res.ResponseFailure.SYSTEM_ERRORassertresponse.message=="test message"

    We add the relevant methods to the class and change the build_from_invalid_request_object() method to leverage the build_parameters_error() new method. Change the rentomatic/shared/response_object.py file to contain this code

    @classmethoddefbuild_resource_error(cls,message=None):returncls(cls.RESOURCE_ERROR,message)@classmethoddefbuild_system_error(cls,message=None):returncls(cls.SYSTEM_ERROR,message)@classmethoddefbuild_parameters_error(cls,message=None):returncls(cls.PARAMETERS_ERROR,message)@classmethoddefbuild_from_invalid_request_object(cls,invalid_request_object):message="\n".join(["{}: {}".format(err['parameter'],err['message'])forerrininvalid_request_object.errors])returncls.build_parameters_error(message)

    Use cases (part 3)

    Git tag: step09

    Our implementation of responses and requests is finally complete, so now we can implement the last version of our use case. The use case correctly returns a ResponseSuccess object but is still missing a proper validation of the incoming request.

    Let's change the test in the tests/use_cases/test_storageroom_list_use_case.py file and add two more tests. The resulting set of tests (after the domain_storagerooms fixture) is the following

    importpytestfromunittestimportmockfromrentomatic.domain.storageroomimportStorageRoomfromrentomatic.sharedimportresponse_objectasresfromrentomatic.use_casesimportrequest_objectsasreqfromrentomatic.use_casesimportstorageroom_use_casesasuc@pytest.fixturedefdomain_storagerooms():[...]deftest_storageroom_list_without_parameters(domain_storagerooms):repo=mock.Mock()repo.list.return_value=domain_storageroomsstorageroom_list_use_case=uc.StorageRoomListUseCase(repo)request_object=req.StorageRoomListRequestObject.from_dict({})response_object=storageroom_list_use_case.execute(request_object)assertbool(response_object)isTruerepo.list.assert_called_with(filters=None)assertresponse_object.value==domain_storagerooms

    This is the test we already wrote, but the assert_called_with() method is called with filters=None to reflect the added parameter. The import line has slightly changed as well, given that we are now importing both response_objects and request_objects. The domain_storagerooms fixture has not changed and has been omitted from the code snippet to keep it short.

    deftest_storageroom_list_with_filters(domain_storagerooms):repo=mock.Mock()repo.list.return_value=domain_storageroomsstorageroom_list_use_case=uc.StorageRoomListUseCase(repo)qry_filters={'a':5}request_object=req.StorageRoomListRequestObject.from_dict({'filters':qry_filters})response_object=storageroom_list_use_case.execute(request_object)assertbool(response_object)isTruerepo.list.assert_called_with(filters=qry_filters)assertresponse_object.value==domain_storagerooms

    This test checks that the value of the filters key in the dictionary used to create the request is actually used when calling the repository.

    deftest_storageroom_list_handles_generic_error():repo=mock.Mock()repo.list.side_effect=Exception('Just an error message')storageroom_list_use_case=uc.StorageRoomListUseCase(repo)request_object=req.StorageRoomListRequestObject.from_dict({})response_object=storageroom_list_use_case.execute(request_object)assertbool(response_object)isFalseassertresponse_object.value=={'type':res.ResponseFailure.SYSTEM_ERROR,'message':"Exception: Just an error message"}deftest_storageroom_list_handles_bad_request():repo=mock.Mock()storageroom_list_use_case=uc.StorageRoomListUseCase(repo)request_object=req.StorageRoomListRequestObject.from_dict({'filters':5})response_object=storageroom_list_use_case.execute(request_object)assertbool(response_object)isFalseassertresponse_object.value=={'type':res.ResponseFailure.PARAMETERS_ERROR,'message':"filters: Is not iterable"}

    This last two tests check the behaviour of the use case when the repository raises an exception or when the request is badly formatted.

    Change the file rentomatic/use_cases/storageroom_use_cases.py to contain the new use case implementation that makes all the test pass

    fromrentomatic.sharedimportresponse_objectasresclassStorageRoomListUseCase(object):def__init__(self,repo):self.repo=repodefexecute(self,request_object):ifnotrequest_object:returnres.ResponseFailure.build_from_invalid_request_object(request_object)try:storage_rooms=self.repo.list(filters=request_object.filters)returnres.ResponseSuccess(storage_rooms)exceptExceptionasexc:returnres.ResponseFailure.build_system_error("{}: {}".format(exc.__class__.__name__,"{}".format(exc)))

    As you can see the first thing that the execute() method does is to check if the request is valid, otherwise returns a ResponseFailure build with the same request object. Then the actual business logic is implemented, calling the repository and returning a success response. If something goes wrong in this phase the exception is caught and returned as an aptly formatted ResponseFailure.

    Intermezzo: refactoring

    Git tag: step10

    A clean architecture is not a framework, so it provides very few generic features, unlike products like for example Django, which provide models, ORM, and all sorts of structures and libraries. Nevertheless, some classes can be isolated from our code and provided as a library, so that we can reuse the code. In this section I will guide you through a refactoring of the code we already have, during which we will isolate common features for requests, responses, and use cases.

    We already isolated the response object. We can move the test_valid_request_object_cannot_be_used from tests/use_cases/test_storageroom_list_request_objects.py to tests/shared/test_response_object.py since it tests a generic behaviour and not something related to the StorageRoom model and use cases.

    Then we can move the InvalidRequestObject and ValidRequestObject classes from rentomatic/use_cases/request_objects.py to rentomatic/shared/request_object.py, making the necessary changes to the StorageRoomListRequestObject class that now inherits from an external class.

    The use case is the class that undergoes the major changes. The UseCase class is tested by the following code in the tests/shared/test_use_case.py file

    fromunittestimportmockfromrentomatic.sharedimportrequest_objectasreq,response_objectasresfromrentomatic.sharedimportuse_caseasucdeftest_use_case_cannot_process_valid_requests():valid_request_object=mock.MagicMock()valid_request_object.__bool__.return_value=Trueuse_case=uc.UseCase()response=use_case.execute(valid_request_object)assertnotresponseassertresponse.type==res.ResponseFailure.SYSTEM_ERRORassertresponse.message== \
            'NotImplementedError: process_request() not implemented by UseCase class'

    This test checks that the UseCase class cannot be actually used to process incoming requests.

    deftest_use_case_can_process_invalid_requests_and_returns_response_failure():invalid_request_object=req.InvalidRequestObject()invalid_request_object.add_error('someparam','somemessage')use_case=uc.UseCase()response=use_case.execute(invalid_request_object)assertnotresponseassertresponse.type==res.ResponseFailure.PARAMETERS_ERRORassertresponse.message=='someparam: somemessage'

    This test runs the use case with an invalid request and check if the response is correct. Since the request is wrong the response type is PARAMETERS_ERROR, as this represents an issue in the request parameters.

    deftest_use_case_can_manage_generic_exception_from_process_request():use_case=uc.UseCase()classTestException(Exception):passuse_case.process_request=mock.Mock()use_case.process_request.side_effect=TestException('somemessage')response=use_case.execute(mock.Mock)assertnotresponseassertresponse.type==res.ResponseFailure.SYSTEM_ERRORassertresponse.message=='TestException: somemessage'

    This test makes the use case raise an exception. This type of error is categorized as SYSTEM_ERROR, which is a generic name for an exception which is not related to request parameters or actual entities.

    As you can see in this last test the idea is that of exposing the execute() method in the UseCase class and to call the process_request() method defined by each child class, which is the actual use case we are implementing.

    The rentomatic/shared/use_case.py file contains the following code that makes the test pass

    fromrentomatic.sharedimportresponse_objectasresclassUseCase(object):defexecute(self,request_object):ifnotrequest_object:returnres.ResponseFailure.build_from_invalid_request_object(request_object)try:returnself.process_request(request_object)exceptExceptionasexc:returnres.ResponseFailure.build_system_error("{}: {}".format(exc.__class__.__name__,"{}".format(exc)))defprocess_request(self,request_object):raiseNotImplementedError("process_request() not implemented by UseCase class")

    While the rentomatic/use_cases/storageroom_use_cases.py now contains the following code

    fromrentomatic.sharedimportuse_caseasucfromrentomatic.sharedimportresponse_objectasresclassStorageRoomListUseCase(uc.UseCase):def__init__(self,repo):self.repo=repodefprocess_request(self,request_object):domain_storageroom=self.repo.list(filters=request_object.filters)returnres.ResponseSuccess(domain_storageroom)

    The repository layer

    Git tag: step11

    The repository layer is the one in which we run the data storage system. As you saw when we implemented the use case we access the data storage through an API, in this case the list() method of the repository. The level of abstraction provided by a repository level is higher than that provided by an ORM or by a tool like SQLAlchemy. The repository layer provides only the endpoints that the application needs, with an interface which is tailored on the specific business problems the application implements.

    To clarify the matter in terms of concrete technologies, SQLAlchemy is a wonderful tool to abstract the access to an SQL database, so the internal implementation of the repository layer could use it to access a PostgreSQL database. But the external API of the layer is not that provided by SQLAlchemy. The API is a (usually reduced) set of functions that the use cases call to get the data, and indeed the internal implementation could also use raw SQL queries on a proprietary network interface. The repository does not even need to be based on a database. We can have a repository layer that fetches data from a REST service, for example, or that makes remote procedure calls through a RabbitMQ network.

    A very important feature of the repository layer is that it always returns domain models, and this is in line with what framework ORMs usually do.

    I will not deploy a real database in this post. I will address that part of the application in a future post, where there will be enough space to implement two different solutions and show how the repository API can mask the actual implementation.

    Instead, I am going to create a very simple memory storage system with some predefined data. I think this is enough for the moment to demonstrate the repository concept.

    The first thing to do is to write some tests that document the public API of the repository. The file containing the tests is tests/repository/test_memrepo.py.

    First we add some data that we will be using in the tests. We import the domain model to check if the results of the API calls have the correct type

    importpytestfromrentomatic.shared.domain_modelimportDomainModelfromrentomatic.repositoryimportmemrepostorageroom1={'code':'f853578c-fc0f-4e65-81b8-566c5dffa35a','size':215,'price':39,'longitude':'-0.09998975','latitude':'51.75436293',}storageroom2={'code':'fe2c3195-aeff-487a-a08f-e0bdc0ec6e9a','size':405,'price':66,'longitude':'0.18228006','latitude':'51.74640997',}storageroom3={'code':'913694c6-435a-4366-ba0d-da5334a611b2','size':56,'price':60,'longitude':'0.27891577','latitude':'51.45994069',}storageroom4={'code':'eed76e77-55c1-41ce-985d-ca49bf6c0585','size':93,'price':48,'longitude':'0.33894476','latitude':'51.39916678',}@pytest.fixturedefstoragerooms():return[storageroom1,storageroom2,storageroom3,storageroom4]

    Since the repository object will return domain models, we need a helper function to check the correctness of the results. The following function checks the length of the two lists, ensures that all the returned elements are domain models and compares the codes. Note that we can safely employ the isinstance() built-in function since DomainModel is an abstract base class and our models are registered (see the rentomatic/domain/storagerooms.py)

    def_check_results(domain_models_list,data_list):assertlen(domain_models_list)==len(data_list)assertall([isinstance(dm,DomainModel)fordmindomain_models_list])assertset([dm.codefordmindomain_models_list])==set([d['code']fordindata_list])

    We need to be able to initialize the repository with a list of dictionaries, and the list() method without any parameter shall return the same list of entries.

    deftest_repository_list_without_parameters(storagerooms):repo=memrepo.MemRepo(storagerooms)assertrepo.list()==storagerooms

    The list() method shall accept a filters parameter, which is a dictionary. The dictionary keys shall be in the form <attribute>__<operator>, similar to the syntax used by the Django ORM. So to express that the price shall be less than 65 we can write filters={'price__lt': 60}.

    A couple of error conditions shall be checked: using an unknown key shall raise a KeyError exception, and using a wrong operator shall raise a ValueError exception.

    deftest_repository_list_with_filters_unknown_key(storagerooms):repo=memrepo.MemRepo(storagerooms)withpytest.raises(KeyError):repo.list(filters={'name':'aname'})deftest_repository_list_with_filters_unknown_operator(storagerooms):repo=memrepo.MemRepo(storagerooms)withpytest.raises(ValueError):repo.list(filters={'price__in':[20,30]})

    Let us then test that the filtering mechanism actually works. We want the default operator to be __eq, which means that if we do not put any operator an equality check shall be performed.

    deftest_repository_list_with_filters_price(storagerooms):repo=memrepo.MemRepo(storagerooms)_check_results(repo.list(filters={'price':60}),[storageroom3])_check_results(repo.list(filters={'price__eq':60}),[storageroom3])_check_results(repo.list(filters={'price__lt':60}),[storageroom1,storageroom4])_check_results(repo.list(filters={'price__gt':60}),[storageroom2])deftest_repository_list_with_filters_size(storagerooms):repo=memrepo.MemRepo(storagerooms)_check_results(repo.list(filters={'size':93}),[storageroom4])_check_results(repo.list(filters={'size__eq':93}),[storageroom4])_check_results(repo.list(filters={'size__lt':60}),[storageroom3])_check_results(repo.list(filters={'size__gt':400}),[storageroom2])deftest_repository_list_with_filters_code(storagerooms):repo=memrepo.MemRepo(storagerooms)_check_results(repo.list(filters={'code':'913694c6-435a-4366-ba0d-da5334a611b2'}),[storageroom3])

    The implementation of the MemRepo class is pretty simple, and I will not dive into it line by line.

    fromrentomatic.domainimportstorageroomassrclassMemRepo:def__init__(self,entries=None):self._entries=[]ifentries:self._entries.extend(entries)def_check(self,element,key,value):if'__'notinkey:key=key+'__eq'key,operator=key.split('__')ifoperatornotin['eq','lt','gt']:raiseValueError('Operator {} is not supported'.format(operator))operator='__{}__'.format(operator)returngetattr(element[key],operator)(int(value))deflist(self,filters=None):ifnotfilters:returnself._entriesresult=[]result.extend(self._entries)forkey,valueinfilters.items():result=[eforeinresultifself._check(e,key,value)]return[sr.StorageRoom.from_dict(r)forrinresult]

    Update: the original version of this class had a bug in the _check() method, probably a punishment since I stated that the implementation is pretty simple =) The code

    returngetattr(element[key],operator)(int(value))

    was originally

    returngetattr(element[key],operator)(value)

    and this wasn't working as the REST layer passes a string, coming from the parse of the query string.

    The _check() method is not tested, being an internal method. Is this reasonable? Well, in a simple implementation it might be. A proper repository implementation, however, should be tested very well, and such an error would be spotted immediately.

    The REST layer (part1)

    Git tag: step12

    This is the last step of our journey into the clean architecture. We created the domain models, the serializers, the use cases and the repository. We are actually missing an interface that glues everything together, that is gets the call parameters from the user, initializes a use case with a repository, runs the use case that fetches the domain models from the repository and converts them to a standard format. This layer can be represented by a wide number of interfaces and technologies. For example a command line interface (CLI) can implement exactly those steps, getting the parameters via command line switches, and returning the results as plain text on the console. The same underlying system, however, can be leveraged by a web page that gets the call parameters from a set of widgets, perform the steps described above, and parses the returned JSON data to show the result on the same page.

    Whatever technology we want to use to interact with the user to collect inputs and provide results we need to interface with the clean architecture we just built, so now we will create a layer to expose an HTTP API. This can be done with a server that exposes a set of HTTP addresses (API endpoints) that once accessed return some data. Such a layer is commonly called a REST layer, because usually the semantic of the addresses comes from the REST recommendations.

    Flask is a lightweight web server with a modular structure that provides just the parts that the user needs. In particular, we will not use any database/ORM, since we already implemented our own repository layer.

    Please keep in mind that this part of the project, together with the repository layer, is usually implemented as a separate package, and I am keeping them together just for the sake of this introductory tutorial.

    Let us start updating the requirements files. The prod.txt file shall contain Flask

    Flask
    

    The dev.txt file will contain the Flask-Script extension

    -r test.txt
    
    pip
    wheel
    flake8
    Sphinx
    Flask-Script
    

    And the test.txt file will contain the pytest extension to work with Flask (more on this later)

    -r prod.txt
    
    pytest
    tox
    coverage
    pytest-cov
    pytest-flask
    

    Remember to run pip install -r requirements/dev.txt again after those changes to actually install the new packages in your virtual environment.

    The setup of a Flask application is not complex, but a lot of concepts are involved, and since this is not a tutorial on Flask I will run quickly through these steps. I will however provide links to the Flask documentation for every concept.

    I usually define different configurations for my testing, development, and production environments. Since the Flask application can be configured using a plain Python object (documentation), I create the file rentomatic/settings.py to host those objects

    importosclassConfig(object):"""Base configuration."""APP_DIR=os.path.abspath(os.path.dirname(__file__))# This directoryPROJECT_ROOT=os.path.abspath(os.path.join(APP_DIR,os.pardir))classProdConfig(Config):"""Production configuration."""ENV='prod'DEBUG=FalseclassDevConfig(Config):"""Development configuration."""ENV='dev'DEBUG=TrueclassTestConfig(Config):"""Test configuration."""ENV='test'TESTING=TrueDEBUG=True

    Read this page to know more about Flask configuration parameters. Now we need a function that initializes the Flask application (documentation), configures it and registers the blueprints (documentation). The file rentomatic/app.py contains the following code

    fromflaskimportFlaskfromrentomatic.restimportstorageroomfromrentomatic.settingsimportDevConfigdefcreate_app(config_object=DevConfig):app=Flask(__name__)app.config.from_object(config_object)app.register_blueprint(storageroom.blueprint)returnapp

    The application endpoints need to return a Flask Response object, with the actual results and an HTTP status. The content of the response, in this case, is the JSON serialization of the use case response.

    Let us write a test step by step, so that you can perfectly understand what is going to happen in the REST endpoint. The basic structure of the test is

    [SOME PREPARATION]
    [CALL THE API ENDPOINT]
    [CHECK RESPONSE DATA]
    [CHECK RESPONDSE STATUS CODE]
    [CHECK RESPONSE MIMETYPE]
    

    So our first test tests/rest/test_get_storagerooms_list.py is made of the following parts

    @mock.patch('rentomatic.use_cases.storageroom_use_cases.StorageRoomListUseCase')deftest_get(mock_use_case,client):mock_use_case().execute.return_value=res.ResponseSuccess(storagerooms)

    Remember that we are not testing the use case here, so we can safely mock it. Here we make the use case return a ResponseSuccess instance containing a list of domain models (that we didn't define yet).

    http_response=client.get('/storagerooms')

    This is the actual API call. We are exposing the endpoint at the /storagerooms address. Note the use of the client fixture provided by pytest-flask.

    assertjson.loads(http_response.data.decode('UTF-8'))==[storageroom1_dict]asserthttp_response.status_code==200asserthttp_response.mimetype=='application/json'

    These are the three checks previously mentioned. The second and the third ones are pretty straightforward, while the first one needs some explanations. We want to compare http_response.data with [storageroom1_dict], which is a list with a Python dictionary containing the data of the storageroom1_domain_model object. Flask Response objects contain a binary representation of the data, so first we decode the bytes using UTF-8, then convert them in a Python object. It is much more convenient to compare Python objects, since pytest can deal with issues like the unordered nature of dictionaries, while this is not possible when comparing two strings.

    The final test file, with the test domain model and its dictionary is

    importjsonfromunittestimportmockfromrentomatic.domain.storageroomimportStorageRoomfromrentomatic.sharedimportresponse_objectasresstorageroom1_dict={'code':'3251a5bd-86be-428d-8ae9-6e51a8048c33','size':200,'price':10,'longitude':-0.09998975,'latitude':51.75436293}storageroom1_domain_model=StorageRoom.from_dict(storageroom1_dict)storagerooms=[storageroom1_domain_model]@mock.patch('rentomatic.use_cases.storageroom_use_cases.StorageRoomListUseCase')deftest_get(mock_use_case,client):mock_use_case().execute.return_value=res.ResponseSuccess(storagerooms)http_response=client.get('/storagerooms')assertjson.loads(http_response.data.decode('UTF-8'))==[storageroom1_dict]asserthttp_response.status_code==200asserthttp_response.mimetype=='application/json'

    It's time to write the endpoint, where we will finally see all the pieces of the architecture working together.

    The minimal Flask endpoint we can put in rentomatic/rest/storageroom.py is something like

    blueprint=Blueprint('storageroom',__name__)@blueprint.route('/storagerooms',methods=['GET'])defstorageroom():[LOGIC]returnResponse([JSONDATA],mimetype='application/json',status=[STATUS])

    The first part of our logic is the creation of a StorageRoomListRequestObject. For the moment we can ignore the optional querystring parameters and use an empty dictionary

    defstorageroom():request_object=ro.StorageRoomListRequestObject.from_dict({})

    As you can see I'm creating the object from an empty dictionary, so querystring parameters are not taken into account for the moment. The second thing to do is to initialize the repository

    repo=mr.MemRepo()

    The third thing the endpoint has to do is the initialization of the use case

    use_case=uc.StorageRoomListUseCase(repo)

    And finally we run the use case passing the request object

    response=use_case.execute(request_object)

    This response, however, is not yet an HTTP response, and we have to explicitly build it. The HTTP response will contain the JSON representation of the response.value attribute.

    returnResponse(json.dumps(response.value,cls=ser.StorageRoomEncoder),mimetype='application/json',status=200)

    Note that this function is obviously still incomplete, as it returns always a successful response (code 200). It is however enough to pass the test we wrote. The whole file is the following

    importjsonfromflaskimportBlueprint,Responsefromrentomatic.use_casesimportrequest_objectsasreqfromrentomatic.repositoryimportmemrepoasmrfromrentomatic.use_casesimportstorageroom_use_casesasucfromrentomatic.serializersimportstorageroom_serializerasserblueprint=Blueprint('storageroom',__name__)@blueprint.route('/storagerooms',methods=['GET'])defstorageroom():request_object=req.StorageRoomListRequestObject.from_dict({})repo=mr.MemRepo()use_case=uc.StorageRoomListUseCase(repo)response=use_case.execute(request_object)returnResponse(json.dumps(response.value,cls=ser.StorageRoomEncoder),mimetype='application/json',status=200)

    This code demonstrates how the clean architecture works in a nutshell. The function we wrote is however not complete, as it doesn't consider querystring parameters and error cases.

    The server in action

    Git tag: step13

    Before I fix the missing parts of the endpoint let us see the server in action, so we can finally enjoy the product we have been building during this long post.

    To actually see some results when accessing the endpoint we need to fill the repository with some data. This part is obviously required only because of the ephemeral nature of the repository we are using. A real repository would wrap a persistent source of data and providing data at this point wouldn't be necessary. To initialize the repository we have to define some data

    storageroom1={'code':'f853578c-fc0f-4e65-81b8-566c5dffa35a','size':215,'price':39,'longitude':'-0.09998975','latitude':'51.75436293',}storageroom2={'code':'fe2c3195-aeff-487a-a08f-e0bdc0ec6e9a','size':405,'price':66,'longitude':'0.18228006','latitude':'51.74640997',}storageroom3={'code':'913694c6-435a-4366-ba0d-da5334a611b2','size':56,'price':60,'longitude':'0.27891577','latitude':'51.45994069',}

    And them feed them into the repository

    repo=mr.MemRepo([storageroom1,storageroom2,storageroom3])

    Now we can run the Flask manage.py file to check the exposed URLs

    $ python manage.py urls
    Rule                     Endpoint               
    ------------------------------------------------
    /static/<path:filename>  static                 
    /storagerooms            storageroom.storageroom
    

    and run the development server

    $ python manage.py server
    

    If you open your browser and navigate to http://127.0.0.1:5000/storagerooms, you can see the API call results. I recommend installing a formatter extension for the browser to better check the output. If you are using Chrome try JSON Formatter.

    The REST layer (part2)

    Git tag: step14

    Let us cover the two missing cases in the endpoint. First I introduce a test to check if the endpoint correctly handles querystring parameters

    @mock.patch('rentomatic.use_cases.storageroom_use_cases.StorageRoomListUseCase')deftest_get_failed_response(mock_use_case,client):mock_use_case().execute.return_value=res.ResponseFailure.build_system_error('test message')http_response=client.get('/storagerooms')assertjson.loads(http_response.data.decode('UTF-8'))=={'type':'SYSTEM_ERROR','message':'test message'}asserthttp_response.status_code==500asserthttp_response.mimetype=='application/json'

    This makes the use case return a failed response and check that the HTTP response contains a formatted version of the error. To make this test pass we have to introduce a proper mapping between domain responses codes and HTTP codes

    fromrentomatic.sharedimportresponse_objectasresSTATUS_CODES={res.ResponseSuccess.SUCCESS:200,res.ResponseFailure.RESOURCE_ERROR:404,res.ResponseFailure.PARAMETERS_ERROR:400,res.ResponseFailure.SYSTEM_ERROR:500}

    Then we need to create the Flask response with the correct code

    returnResponse(json.dumps(response.value,cls=ser.StorageRoomEncoder),mimetype='application/json',status=STATUS_CODES[response.type])

    The second and last test is a bit more complex. As before we will mock the use case, but this time we will also patch StorageRoomListRequestObject. We need in fact to know if the request object is initialized with the correct parameters from the command line. So, step by step

    @mock.patch('rentomatic.use_cases.storageroom_use_cases.StorageRoomListUseCase')deftest_request_object_initialisation_and_use_with_filters(mock_use_case,client):mock_use_case().execute.return_value=res.ResponseSuccess([])

    This is, like, before, a patch of the use case class that ensures the use case will return a ResponseSuccess instance.

    internal_request_object=mock.Mock()

    The request object will be internally created with StorageRoomListRequestObject.from_dict, and we want that function to return a known mock object, which is the one we initialized here.

    request_object_class='rentomatic.use_cases.request_objects.StorageRoomListRequestObject'withmock.patch(request_object_class)asmock_request_object:mock_request_object.from_dict.return_value=internal_request_objectclient.get('/storagerooms?filter_param1=value1&filter_param2=value2')

    Here we patch StorageRoomListRequestObject and we assign a known output to the from_dict() method. Then we call the endpoint with some querystring parameters. What should happen is that the from_dict() method of the request is called with the filter parameters and that the execute() method of the use case instance is called with the internal_request_object.

    mock_request_object.from_dict.assert_called_with({'filters':{'param1':'value1','param2':'value2'}})mock_use_case().execute.assert_called_with(internal_request_object)

    The endpoint function shall be changed somehow to reflect this new behaviour and to make the test pass. The whole code of the new storageroom() Flask method is the following

    importjsonfromflaskimportBlueprint,request,Responsefromrentomatic.use_casesimportrequest_objectsasreqfromrentomatic.sharedimportresponse_objectasresfromrentomatic.repositoryimportmemrepoasmrfromrentomatic.use_casesimportstorageroom_use_casesasucfromrentomatic.serializersimportstorageroom_serializerasserblueprint=Blueprint('storageroom',__name__)STATUS_CODES={res.ResponseSuccess.SUCCESS:200,res.ResponseFailure.RESOURCE_ERROR:404,res.ResponseFailure.PARAMETERS_ERROR:400,res.ResponseFailure.SYSTEM_ERROR:500}storageroom1={'code':'f853578c-fc0f-4e65-81b8-566c5dffa35a','size':215,'price':39,'longitude':'-0.09998975','latitude':'51.75436293',}storageroom2={'code':'fe2c3195-aeff-487a-a08f-e0bdc0ec6e9a','size':405,'price':66,'longitude':'0.18228006','latitude':'51.74640997',}storageroom3={'code':'913694c6-435a-4366-ba0d-da5334a611b2','size':56,'price':60,'longitude':'0.27891577','latitude':'51.45994069',}@blueprint.route('/storagerooms',methods=['GET'])defstorageroom():qrystr_params={'filters':{},}forarg,valuesinrequest.args.items():ifarg.startswith('filter_'):qrystr_params['filters'][arg.replace('filter_','')]=valuesrequest_object=req.StorageRoomListRequestObject.from_dict(qrystr_params)repo=mr.MemRepo([storageroom1,storageroom2,storageroom3])use_case=uc.StorageRoomListUseCase(repo)response=use_case.execute(request_object)returnResponse(json.dumps(response.value,cls=ser.StorageRoomEncoder),mimetype='application/json',status=STATUS_CODES[response.type])

    Note that we extract the querystring parameters from the global request object provided by Flask. Once the querystring parameters are in a dictionary, we just need to create the request object from it.

    Conclusions

    Well, that's all! Some tests are missing in the REST part, but as I said I just wanted to show a working implementation of a clean architecture and not a fully developed project. I suggest that you try to implement some changes, for example:

    • another endpoint like the access to a single resource (/storagerooms/<code>)
    • a different repository, connected to a real DB (you can use SQLite, for example)
    • implement a new querystring parameter, for example the distance from a given point on the map (use geopy to easily compute distances)

    While you develop your code always try to work following the TDD approach. Testability is one of the main features of a clean architecture, so don't ignore it.

    Whether you decide to use a clean architecture or not, I really hope this post helped you to get a fresh view on software architectures, as happened to me when I first discovered the concepts exemplified here.

    Updates

    2016-11-15: Two tests contained variables with a wrong name (artist), which came from an initial version of the project. The name did not affect the tests. Added some instructions on the virtual environment and the development requirements.

    2016-12-12: Thanks to Marco Beri who spotted a typo in the code of step 6, which was already correct in the GitHub repository. He also suggested using the Cookiecutter package by Ardy Dedase. Thanks to Marco and to Ardy!

    2018-06-03: Thanks to mikus that spotted the bug in the _check() method of the MemRepo class. The reason why the bug wasn't spotted before has been clarified in the post, and both the post and the rentomatic repository have been updated. Thanks!

    Feedback

    Feel free to use the blog Google+ page to comment the post. The GitHub issues page is the best place to submit corrections.

    Weekly Python StackOverflow Report: (cxxviii) stackoverflow python report

    $
    0
    0

    Martin Fitzpatrick: Failamp

    $
    0
    0

    Failamp is a simple audio & video mediaplayer implemented in Python, using the built-in Qt playlist and media handling features. It is modelled, very loosely on the original Winamp, although nowhere near as complete (hence the fail).

    Failamp.

    The main window

    The main window UI was built using Qt Designer. The screenshot below shows the constructed layout, with the default colour scheme as visible within Designer.

    UI in Qt Designer.

    The layout is constructed in a QVBoxLayout which in turn contains the playlist view (QListView) and two QHBoxLayout horizontal layouts which contain the time slider and time indicators and the control buttons respectively.

    Player

    First we need to setup the Qt media player controller QMediaPlayer. This controller handles load and playback of media files automatically, we just need to provide it with the appropriate signals.

    We create a persistent player which we'll use globally. We setup an error handler by connecting our custom erroralert slot to the error signal.

    classMainWindow(QMainWindow,Ui_MainWindow):def__init__(self,*args,**kwargs):super(MainWindow,self).__init__(*args,**kwargs)self.setupUi(self)self.player=QMediaPlayer()self.player.error.connect(self.erroralert)

    The generic media player controls can all be connected to the player directly, using the appropriate slots on self.player.

    # Connect control buttons/slides for media player.self.playButton.pressed.connect(self.player.play)self.pauseButton.pressed.connect(self.player.pause)self.stopButton.pressed.connect(self.player.stop)

    We also have two slots for volume control and time slider position. Updating either of these will alter the playback automatically, without any handling on by us.

    self.volumeSlider.valueChanged.connect(self.player.setVolume)self.timeSlider.valueChanged.connect(self.player.setPosition)

    Finally we connect up our timer display methods to the player position signals, allowing us to automatically update the display as the play position changes.

    self.player.durationChanged.connect(self.update_duration)self.player.positionChanged.connect(self.update_position)

    When you drag the slider, this sends a signal to update the play position, which in turn sends a signal to update the time display. Chaining operations off each other allows you to keep your app components independent, one of the great things about signals.

    Qt Multimedia also provides a simple playlist controller. This does not provide the widget itself, just a simple interface for queuing up tracks (we handle the display using our own QListView).

    Playlist

    Helpfully, the playlist can be passed to the player, which wil then use it to automatically select the track to play once the current one is complete.

    self.playlist=QMediaPlaylist()self.player.setPlaylist(self.playlist)

    The previous and next control buttons are connected to the playlist and will perform skip/restart/back as expected (all handled by QMediaPlaylist). Because the playlist is connected to the player, this will automatically trigger the player to play the appropriate track.

    self.previousButton.pressed.connect(self.playlist.previous)self.nextButton.pressed.connect(self.playlist.next)

    The display of the playlist is handled by a QListView object. This is a view component from Qt's model/view architecture, which is used to efficiently display data held in data models. In our case, we are storing the data in a playlist object QMediaPlaylist from the Qt Multimedia module.

    The PlaylistModel is our custom model for taking data from the QMediaPlaylist and mapping it to the view. We instantiate the model and pass it into our view.

    self.model=PlaylistModel(self.playlist)self.playlistView.setModel(self.model)self.playlist.currentIndexChanged.connect(self.playlist_position_changed)selection_model=self.playlistView.selectionModel()selection_model.selectionChanged.connect(self.playlist_selection_changed)

    Opening and dropping

    We have a single file operation — open a file — which adds the file to the playlist. We also accept drag and drop, which is covered later.

    self.open_file_action.triggered.connect(self.open_file)self.setAcceptDrops(True)

    Video

    Finally, we add the viewer for video playback. If we don't add this the player will still play videos, but just play the audio component. Video playback is handled by a specific QVideoWidget. To enable playback we just pass this widget to the players .setVideoOutput method.

    self.viewer=ViewerWindow(self)self.viewer.setWindowFlags(self.viewer.windowFlags()|Qt.WindowStaysOnTopHint)self.viewer.setMinimumSize(QSize(480,360))videoWidget=QVideoWidget()self.viewer.setCentralWidget(videoWidget)self.player.setVideoOutput(videoWidget)

    Finally we just enable the toggles for the viewer to show/hide on demand.

    self.viewButton.toggled.connect(self.toggle_viewer)self.viewer.state.connect(self.viewButton.setChecked)

    Playlist Model

    As mentioned we're using a QListView object from Qt's model/view architecture for playlist display, with data held in the QMediaPlaylist. Since the data store is already handled for us, all we need to handle is the mapping from the playlist to the view.

    In this case the requirements are pretty basic — we need:

    1. a method rowCount to return the total number of rows in the playlist, via .mediaCount()
    2. a method data which returns data for a specific row, in this case we're only displaying the filename

    You could extend this to access media file metadata and show the track name instead.

    classPlaylistModel(QAbstractListModel):def__init__(self,playlist,*args,**kwargs):super(PlaylistModel,self).__init__(*args,**kwargs)self.playlist=playlistdefdata(self,index,role):ifrole==Qt.DisplayRole:media=self.playlist.media(index.row())returnmedia.canonicalUrl().fileName()defrowCount(self,index):returnself.playlist.mediaCount()

    By storing a reference to the playlist in __init__ the can get the other data easily at any time. Changes to the playlist in the application will be automatically reflected in the view.

    The playlist and the player can handle track changes automatically, and we have the controls for skipping. However, we also want users to be able to select a track to play in the playlist, and we want the selection in the playlist view to update automatically as the tracks progress.

    For both of these we need to define our own custom handlers. The first is for updating the playlist position in response to playlist selection by the user —

    defplaylist_selection_changed(self,ix):# We receive a QItemSelection from selectionChanged.i=ix.indexes()[0].row()self.playlist.setCurrentIndex(i)

    The next is to update the selection in the playlist as the track progresses. We specifically check for -1 since this value is sent by the playlist when there are not more tracks to play — either we're at the end of the playlist, or the playlist is empty.

    defplaylist_position_changed(self,i):ifi>-1:ix=self.model.index(i)self.playlistView.setCurrentIndex(ix)

    Drag, drop, and file operations

    We enabled drag and drop on the main window by setting self.setAcceptDrops(True). With this enabled, the main window will raise the dragEnterEvent and dropEvent events when we perform drag-drop operations.

    This enter/drop duo is the standard approach to drag-drop in desktop UIs. The enter event recognises what is being dropped and either accepts or rejects. Only if accepted can a drop occur.

    The dragEnterEvent checks whether the dragged object is droppable on our application. In this implementation we're very lax — we only check that the drop is file (by path). By default QMimeData has checks built in for html, image, text and path/URL types, but not audio or video. If we want these we would have to implement them ourselves.

    We could add a check here for specific file extension based on what we support.

    defdragEnterEvent(self,e):ife.mimeData().hasUrls():e.acceptProposedAction()

    The dropEvent iterates over the URLs in the provided data, and adds them to the playlist. If we're not playing, dropping the file triggers autoplay from the newly added file.

    defdropEvent(self,e):forurline.mimeData().urls():self.playlist.addMedia(QMediaContent(url))self.model.layoutChanged.emit()# If not playing, seeking to first of newly added + play.ifself.player.state()!=QMediaPlayer.PlayingState:i=self.playlist.mediaCount()-len(e.mimeData().urls())self.playlist.setCurrentIndex(i)self.player.play()

    The single operation defined is to open a file, which adds it to the current playlist. We predefine a number of standard audio and video file types — you can easily add more, as long as they are supported by the QMediaPlayer controller, they will work fine.

    defopen_file(self):path,_=QFileDialog.getOpenFileName(self,"Open file","","mp3 Audio (*.mp3);mp4 Video (*.mp4);Movie files (*.mov);All files (*.*)")ifpath:self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile(path)))self.model.layoutChanged.emit()

    Position & duration

    The QMediaPlayer controller emits signals when the current playback duration and position are updated. The former is changed when the current media being played changes, e.g. when we progress to the next track. The second is emitted repeatedly as the play position updates during playback.

    Both receive an int64 (64 bit integer) which represents the time in milliseconds. This same scale is used by all signals so there is no conversion between them, and we can simply pass the value to our slider to update.

    defupdate_duration(self,duration):self.timeSlider.setMaximum(duration)ifduration>=0:self.totalTimeLabel.setText(hhmmss(duration))

    One slightly tricky thing occurs where we update the slider position. We want to update the slider as the track progresses, however updating the slider triggers the update of the position (so the user can drag to a position in the track). This can trigger weird behaviour and a possible endless loop.

    To work around it we just block the signals while we make the update, and re-enable the after.

    defupdate_position(self,position):ifposition>=0:self.currentTimeLabel.setText(hhmmss(position))# Disable the events to prevent updating triggering a setPosition event (can cause stuttering).self.timeSlider.blockSignals(True)self.timeSlider.setValue(position)self.timeSlider.blockSignals(False)

    Video viewer

    The video viewer is a simple QMainWindow with the addition of a toggle handler to show/display the window. We also add a hook into the closeEvent to update the toggle button, while overriding the default behaviour — closing the window will not actually close it, just hide it.

    classViewerWindow(QMainWindow):state=pyqtSignal(bool)defcloseEvent(self,e):# Emit the window state, to update the viewer toggle button.self.state.emit(False)deftoggle_viewer(self,state):ifstate:self.viewer.show()else:self.viewer.hide()

    Style

    To mimic the style of Winamp (badly) we're using the Fusion application style as a base, then applying a dark theme. The Fusion style is nice Qt cross-platform application style. The dark theme has been borrowed from this Gist from user QuantumCD.

    app.setStyle("Fusion")palette=QPalette()# Get a copy of the standard palette.palette.setColor(QPalette.Window,QColor(53,53,53))palette.setColor(QPalette.WindowText,Qt.white)palette.setColor(QPalette.Base,QColor(25,25,25))palette.setColor(QPalette.AlternateBase,QColor(53,53,53))palette.setColor(QPalette.ToolTipBase,Qt.white)palette.setColor(QPalette.ToolTipText,Qt.white)palette.setColor(QPalette.Text,Qt.white)palette.setColor(QPalette.Button,QColor(53,53,53))palette.setColor(QPalette.ButtonText,Qt.white)palette.setColor(QPalette.BrightText,Qt.red)palette.setColor(QPalette.Link,QColor(42,130,218))palette.setColor(QPalette.Highlight,QColor(42,130,218))palette.setColor(QPalette.HighlightedText,Qt.black)app.setPalette(palette)# Additional CSS styling for tooltip elements.app.setStyleSheet("QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }")

    This covers all the elements used in this Failamp. If you want to use this is another app you may need to add additional CSS tweaks, like that added for QToolTip.

    Timer

    Finally, we need a method to convert a time in milliseconds in to an h:m:s or m:s display. For this we can use a series of divmod calls with the milliseconds for each time division. This returns the number of complete divisions (div) and the remainder (mod). The slight tweak is to only show the hour part when the time is longer than an hour.

    defhhmmss(ms):# s = 1000# m = 60000# h = 360000h,r=divmod(ms,36000)m,r=divmod(r,60000)s,_=divmod(r,1000)return("%d:%02d:%02d"%(h,m,s))ifhelse("%d:%02d"%(m,s))

    Further ideas

    A few nice improvements for this would be —

    1. Auto-displaying the video viewer when viewing video (and auto-hiding when not)
    2. Docking of windows so they can be snapped together — like the original Winamp.
    3. Graphic equalizer/display — QMediaPlayer does provide a stream of the audio data which is playing. With numpy for FFT we should be able to create a nice nice visual.

    Mike Driscoll: PyDev of the Week: Maria Camila Remolina Gutiérrez

    $
    0
    0

    This week we welcome Maria Camila Remolina Gutiérrez (@holamariacamila) as our PyDev of the Week! Maria recently gave a talk at PyCon USA in their new PyCon Charlas track last month. You can learn more about Maria on her website or you can check out her Github profile to see what she has been doing in the open source world. Let’s take a few minutes to get to know her better!

    Can you tell us a little about yourself (hobbies, education, etc):

    My name is Maria Camila. I am a 23 year old colombian. I am a Physicist and soon to be Systems and Computing Engineer (I will graduate in June 2018). I am studying at Universidad de los Andes in Bogotá, Colombia. My main area of research so far is computational astrophysics, especially simulations. I am very interested in the topics of infrastructure, high performance computing and security. Regarding my hobbies, I love pottery and ceramics, I have been doing it for 1.5 years so far. Also, I’ve been trying to learn bongo drums in my spare time. Finally, I love learning new languages, especially because it is binded to the culture of the countries that speak them, so it allows you to discover the world in a different way.

    Why did you start using Python?

    Python was my first programming language. I used it first as a senior in high school at a Summer Science Program, in order to implement Gauss’ orbit determination method. Me and my team took 3 observations of a Near Earth Object and we wanted to obtain its orbital elements. We used python to implement all the algorithm and data processing. It was also my first time programming, so I loved how Python made it easier to me to get started in this area.

    What other programming languages do you know and which is your favorite?

    Besides Python I know: C, C++, Java, Javascript, Kotlin and a little bit of R. I really want to learn Rust in the summer. And from all my favorite is Python <3

    What projects are you working on now?

    I am finishing my undergrad education, I graduate this summer. I am preparing to start my master in computer science in Paris, France in the fall. My two current and main projects are my research in astronomy and my undergrad engineering thesis. For the first, I am in the process of publishing my first paper as first author (:D) on simulations of rotating Lyman Alpha galaxies. For the second, I finished an implementation of a program for civil engineers that creates a minimum cost design of a sewers system, all using parallel computation.

    Which Python libraries are your favorite (core or 3rd party)?

    Numpy, Scipy, Matplotlib and Pandas! I estimate that 90% of my imports are these 4 libraries. They are fundamental for my data processing, analysis and plotting. I’ve used them in all my research for almost 5 years already.

    How did you become a part of the PyCon Charlas track this year?

    This is an interesting story. I went to give a talk at Pycon Colombia (my first Pycon ever). Naomi Ceder and Lorena Mesa were there as keynote speakers, but also promoting the PSF. I went to their booth to talk to them (and also get some stickers!) and they were giving out small flyers with the Pycon Charlas promotion. They really motivated me to apply and get financial aid for the travel. And so I did. I was honored to be selected and also funded by PyLadies. This (huge) Pycon has been one of the greatest experience I’ve had! (Here’s a pic that proves it)

    What is your motivation for working in open source?

    My motivation is that one of the world’s greatest problems is inequality. However, I believe the current state of computation and technology has opened and era where accessing information is definitely easier. Now you don’t have to pay college, not even go to high school. All you need/want to know is at the tip of your fingers. However, not everything can be free, and this is not wrong, it’s actually awesome how this industry gives people jobs. But this means that there is still software -really good software that could boost someone’s life- that you can only get by paying. So I think that giving people the power of these private tools for free, is the best way to grow our society. What motivates me it’s that what differentiates people is not their family inheritance, but their own personal work and effort. And open source allows this!

    Is there anything else you’d like to say?

    Going to Pycon 2018 showed me how incredibly strong and kind is the Python community. I had a rough idea from online interactions, but I never imagined it to be this big. I think this is the best part of the language. That it joins people together, from all around the world, to create new products that exceed our expectations. And also to be humans together. I loved how there were open spaces to talk about mental health, or to do cross stitching, there was even one to play board games! I think we should never forget to be people before programmers. And I believe all the pythonistas I’ve met are exactly that.

    Thanks for doing the interview, Maria!

    Montreal Python User Group: Montréal-Python 72 - Carroty Xenophon

    $
    0
    0

    Let’s meet one last time before our Summer break! Thanks to Notman House for sponsoring this event.

    Presentations

    Socket - Éric Lafontaine

    Most of our everyday job include doing request over the internet or hosting a web solution for our company. Each connections we make utilize the socket API in some way that is not always evident. I hope to, by giving this talk, elucidate some of the magic contained in the socket API. I'm also going to give away some trick that I've been using since understanding that API.

    Probabilistic Programming and Bayesian Modeling with PyMC3 - Christopher Fonnesbeck

    Bayesian statistics offers powerful, flexible methods for data analysis that, because they are based on full probability models, confer several benefits to analysts including scalability, straightforward quantification of uncertainty, and improved interpretability relative to classical methods. The advent of probabilistic programming has served to abstract the complexity associated with fitting Bayesian models, making such methods more widely available. PyMC3 is software for probabilistic programming in Python that implements several modern, computationally-intensive statistical algorithms for fitting Bayesian models. PyMC3’s intuitive syntax is helpful for new users, and its reliance on the Theano library for fast computation has allowed developers to keep the code base simple, making it easy to extend and expand the software to meet analytic needs. Importantly, PyMC3 implements several next-generation Bayesian computational methods, allowing for more efficient sampling for small models and better approximations to larger models with larger associated dataset. I will demonstrate how to construct, fit and check models in PyMC, using a selection of applied problems as motivation.

    When

    Monday June 11th, 2018 at 6PM

    Where

    Notman House

    51 Sherbrooke West

    Montréal, QC

    H2X 1X2

    Schedule

    • 6:00PM - Doors open
    • 6:30PM - Presentations
    • 8:00PM - End of the event
    • 8:15PM - Benelux

    Full Stack Python: Developing Flask Apps in Docker Containers on macOS

    $
    0
    0

    Adding Docker to your Python and Flaskdevelopment environment can be confusing when you are just getting started with containers. Let's quickly get Docker installed and configured for developing Flask web applications on your local system.

    Our Tools

    This tutorial is written for Python 3. It will work with Python 2 but I have not tested it with the soon-to-be deprecated 2.7 version.

    Docker for Mac is necessary. I recommend the stable release unless you have an explicit purpose for the edge channel.

    Within the Docker container we will use:

    All of the code for the Dockerfile and the Flask app are available open source under the MIT license on GitHub under the docker-flask-mac directory of the blog-code-examples repository. Use the code for your own purposes as much as you like.

    Installing Docker on macOS

    We need to install Docker before we can spin up our Docker containers. If you already have Docker for Mac installed and working, feel free to jump to the next section.

    On your Mac, download the Docker Community Edition (CE) for Mac installer.

    Download the Docker Community Edition for Mac.

    Find the newly-downloaded install within Finder and double click on the file. Follow the installation process, which includes granting administrative privileges to the installer.

    Open Terminal when the installer is done. Test your Docker installation with the --version flag:

    docker --version
    

    If Docker is installed correctly you should see the following output:

    Docker version 17.12.0-ce, build c97c6d6
    

    Note that Docker runs through a system agent you can find in the menu bar.

    Docker agent in the menu bar.

    I have found the Docker agent to take up some precious battery life on my Macbook Pro. If I am not developing and need to max battery time I will close down the agent and start it back up again when I am ready to code.

    Now that Docker is installed let's get to running a container and writing our Flask application.

    Dockerfile

    Docker needs to know what we want in a container, which is where the Dockerfile comes in.

    # this is an official Python runtime, used as the parent image
    FROM python:3.6.4-slim
    
    # set the working directory in the container to /app
    WORKDIR /app
    
    # add the current directory to the container as /app
    ADD . /app
    
    # execute everyone's favorite pip command, pip install -r
    RUN pip install --trusted-host pypi.python.org -r requirements.txt
    
    # unblock port 80 for the Flask app to run on
    EXPOSE 80
    
    # execute the Flask app
    CMD ["python", "app.py"]
    

    Save the Dockerfile so that we can run our next command with the completed contents of the file. On the commandline run:

    docker build -t flaskdock .
    

    The above docker build file uses the -t flag to tag the image with the name of flaskdock.

    If the build worked successfully we can see the image in with the docker image ls command. Give that a try now:

    docker image ls
    

    We should then see our tag name in the images list:

    REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
    flaskdock           latest              24045e0464af        2 minutes ago       165MB
    

    Our image is ready to load up as a container so we can write a quick Flask app that we will use to test our environment by running it within the container.

    Coding A Simple Flask app

    Time to put together a super simple "Hello, World!" Flask web app to test running Python code within our Docker container. Within the current project directory, create a file named app.py with the following contents:

    fromflaskimportFlask,Responseapp=Flask(__name__)@app.route("/")defhello():returnResponse("Hi from your Flask app running in your Docker container!")if__name__=="__main__":app.run("0.0.0.0",port=80,debug=True)

    The above 7 lines of code (not counting blank PEP8-compliant lines) in app.py allow our application to return a simple message when run with the Flask development server.

    We need just one more file to specify our Flask dependency. Create a requirements.txt file within the same directory as app.py:

    flask==1.0.2
    

    Make sure both the app.py and requirements.txt file are saved then we can give the code a try.

    Running the Container

    Now that we have our image in hand along with the Python code in a file we can run the image as a container with the docker run command. Execute the following command, making sure to replace the absolute path for the volume to your own directory.

    docker run -p 5000:80 --volume=/Users/matt/devel/py/flaskdocker:/app flaskdock
    

    If you receive the error python: can't open file 'app.py': [Errno 2] No such file or directory then you likely forgot to chance /Users/matt/devel/py/flaskdocker to the directory where your project files, especially app.py, are located.

    Flask app responding to requests from within a Docker container.

    Everything worked when you see a simple text-based HTTP response like what is shown above in the screenshot of my Chrome browser.

    What's Next?

    We just installed Docker and configured a Flask application to run inside a container. That is just the beginning of how you can integrate Docker into your workflow. I strongly recommend reading the Django with PostgreSQL quickstart that will introduce you to Docker Swarm as well as the core Docker container service.

    Next up take a look at the Docker and deployment pages for more related tutorials.

    Questions? Let me know via a GitHub issue ticket on the Full Stack Python repository, on Twitter @fullstackpython or @mattmakai.

    Do you see a typo, syntax issue or just something that's confusing in this blog post? Fork this page's source on GitHub and submit a pull request with a fix or file an issue ticket on GitHub.

    Full Stack Python: First Steps with Bottle Apps in Docker Containers on macOS

    $
    0
    0

    It can be confusing to figure out how to use Docker containers in your Python and Bottledevelopment environment workflow. This tutorial will quickly show you the exact steps to get Docker up and running on macOS with a working Bottle web application

    Our Tools

    This tutorial is written for Python 3. It may work with Python 2 but it has not been testing with that soon-to-be deprecated 2.7 version. You should really be using Python 3, preferrably the latest release which is currently 3.6.5.

    Docker for Mac is necessary to run Docker containers. I recommend that you use the stable release unless you have an explicit purpose for the edge channel.

    Within the Docker container we will use:

    All for the Dockerfile and the Bottle project are available open source under the MIT license on GitHub under the docker-bottle-mac directory of the blog-code-examples repository.

    Installing Docker on macOS

    We must install Docker before we can spin up our containers. Jump to the next section if you already have Docker for Mac installed and working on your computer.

    On your Mac, download the Docker Community Edition (CE) for Mac installer.

    Download the Docker Community Edition for Mac.

    Find the newly-downloaded install within Finder and double click on the file. Follow the installation process, which includes granting administrative privileges to the installer.

    Open Terminal when the installer is done. Test your Docker installation with the --version flag:

    docker --version
    

    If Docker is installed correctly you should see the following output:

    Docker version 17.12.0-ce, build c97c6d6
    

    Note that Docker runs through a system agent you can find in the menu bar.

    Docker agent in the menu bar.

    I have found the Docker agent to take up some precious battery life on my Macbook Pro. If I am not developing and need to max battery time I will close down the agent and start it back up again when I am ready to code.

    Now that Docker is installed let's get to running a container and writing our Flask application.

    Dockerfile

    Docker needs to know what we want in a container, which is where the Dockerfile comes in.

    # this is an official Python runtime, used as the parent image
    FROM python:3.6.4-slim
    
    # set the working directory in the container to /app
    WORKDIR /app
    
    # add the current directory to the container as /app
    ADD . /app
    
    # execute everyone's favorite pip command, pip install -r
    RUN pip install --trusted-host pypi.python.org -r requirements.txt
    
    # unblock port 80 for the Flask app to run on
    EXPOSE 80
    
    # execute the Flask app
    CMD ["python", "app.py"]
    

    Save the Dockerfile so that we can run our next command with the completed contents of the file. On the commandline run:

    docker build -t flaskdock .
    

    The above docker build file uses the -t flag to tag the image with the name of flaskdock.

    If the build worked successfully we can see the image in with the docker image ls command. Give that a try now:

    docker image ls
    

    We should then see our tag name in the images list:

    REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
    flaskdock           latest              24045e0464af        2 minutes ago       165MB
    

    Our image is ready to load up as a container so we can write a quick Flask app that we will use to test our environment by running it within the container.

    Coding A Simple Flask app

    Time to put together a super simple "Hello, World!" Flask web app to test running Python code within our Docker container. Within the current project directory, create a file named app.py with the following contents:

    fromflaskimportFlask,Responseapp=Flask(__name__)@app.route("/")defhello():returnResponse("Hi from your Flask app running in your Docker container!")if__name__=="__main__":app.run("0.0.0.0",port=80,debug=True)

    The above 7 lines of code (not counting blank PEP8-compliant lines) in app.py allow our application to return a simple message when run with the Flask development server.

    We need just one more file to specify our Flask dependency. Create a requirements.txt file within the same directory as app.py:

    flask==1.0.2
    

    Make sure both the app.py and requirements.txt file are saved then we can give the code a try.

    Running the Container

    Now that we have our image in hand along with the Python code in a file we can run the image as a container with the docker run command. Execute the following command, making sure to replace the absolute path for the volume to your own directory.

    docker run -p 5000:80 --volume=/Users/matt/devel/py/flaskdocker:/app flaskdock
    

    If you receive the error python: can't open file 'app.py': [Errno 2] No such file or directory then you likely forgot to chance /Users/matt/devel/py/flaskdocker to the directory where your project files, especially app.py, are located.

    Flask app responding to requests from within a Docker container.

    Everything worked when you see a simple text-based HTTP response like what is shown above in the screenshot of my Chrome browser.

    What's Next?

    We just installed Docker and configured a Flask application to run inside a container. That is just the beginning of how you can integrate Docker into your workflow. I strongly recommend reading the Django with PostgreSQL quickstart that will introduce you to Docker Swarm as well as the core Docker container service.

    Next up take a look at the Docker and deployment pages for more related tutorials.

    Questions? Let me know via a GitHub issue ticket on the Full Stack Python repository, on Twitter @fullstackpython or @mattmakai.

    Do you see a typo, syntax issue or just something that's confusing in this blog post? Fork this page's source on GitHub and submit a pull request with a fix or file an issue ticket on GitHub.

    Viewing all 23137 articles
    Browse latest View live


    <script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>