├── .gitignore ├── LICENSE ├── README.md ├── ch03-code-editor ├── app01.py └── app02.py ├── ch06-managing-projects ├── advanced │ ├── .gitignore │ ├── LICENSE.rst │ ├── MANIFEST.in │ ├── README.rst │ ├── flaskr │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── blog.py │ │ ├── db.py │ │ ├── schema.sql │ │ ├── static │ │ │ └── style.css │ │ └── templates │ │ │ ├── auth │ │ │ ├── login.html │ │ │ └── register.html │ │ │ ├── base.html │ │ │ └── blog │ │ │ ├── create.html │ │ │ ├── index.html │ │ │ └── update.html │ ├── setup.cfg │ ├── setup.py │ └── tests │ │ ├── conftest.py │ │ ├── data.sql │ │ ├── test_auth.py │ │ ├── test_blog.py │ │ ├── test_db.py │ │ └── test_factory.py ├── medium │ ├── LICENSE │ ├── Makefile │ ├── README.md │ ├── main.py │ ├── medium │ │ ├── __init__.py │ │ ├── db.py │ │ ├── medium.py │ │ └── utils.py │ ├── requirements.txt │ └── tests │ │ ├── test_db.py │ │ ├── test_medium.py │ │ └── test_utils.py └── simple │ ├── LICENSE │ ├── Makefile │ ├── README.md │ ├── requirements.txt │ ├── simple.py │ └── tests.py ├── ch08-testing ├── pyproject.toml ├── pytest_docstring.py ├── pytest_example.py ├── pytest_fixtures.py ├── pytest_mocking.py ├── pytest_parametrize.py ├── pytest_parametrize_calculator.py └── test_string_methods.py ├── ch09-documentation ├── .vscode │ └── settings.json ├── calculator.py ├── docs │ ├── Makefile │ ├── make.bat │ └── source │ │ ├── _build │ │ └── html │ │ │ ├── .buildinfo │ │ │ ├── .doctrees │ │ │ ├── environment.pickle │ │ │ ├── index.doctree │ │ │ ├── quickstart.doctree │ │ │ └── tutorials │ │ │ │ ├── tutorial1.doctree │ │ │ │ └── tutorial2.doctree │ │ │ ├── _sources │ │ │ ├── index.rst.txt │ │ │ ├── quickstart.rst.txt │ │ │ └── tutorials │ │ │ │ ├── tutorial1.rst.txt │ │ │ │ └── tutorial2.rst.txt │ │ │ ├── _static │ │ │ ├── alabaster.css │ │ │ ├── basic.css │ │ │ ├── custom.css │ │ │ ├── doctools.js │ │ │ ├── documentation_options.js │ │ │ ├── file.png │ │ │ ├── jquery-3.5.1.js │ │ │ ├── jquery.js │ │ │ ├── language_data.js │ │ │ ├── minus.png │ │ │ ├── plus.png │ │ │ ├── pygments.css │ │ │ ├── searchtools.js │ │ │ ├── underscore-1.3.1.js │ │ │ └── underscore.js │ │ │ ├── genindex.html │ │ │ ├── index.html │ │ │ ├── objects.inv │ │ │ ├── quickstart.html │ │ │ ├── search.html │ │ │ ├── searchindex.js │ │ │ └── tutorials │ │ │ ├── tutorial1.html │ │ │ └── tutorial2.html │ │ ├── api.rst │ │ ├── conf.py │ │ ├── index.rst │ │ ├── quickstart.rst │ │ └── tutorials │ │ ├── tutorial1.rst │ │ └── tutorial2.rst └── math_operations.py ├── ch10-ci ├── .pre-commit-config.yaml ├── calculator.py ├── math_operations.py ├── test_calculator.py └── tox.ini ├── ch11-cli └── uptimer │ ├── .vscode │ └── settings.json │ ├── Makefile │ ├── README.rst │ ├── make.bat │ ├── poetry.lock │ ├── pyproject.toml │ ├── source │ ├── api.rst │ ├── conf.py │ └── index.rst │ ├── tests │ ├── __init__.py │ └── test_uptimer.py │ └── uptimer │ ├── __init__.py │ └── uptimer.py ├── ch12-package └── uptimer │ ├── .editorconfig │ ├── .github │ └── ISSUE_TEMPLATE.md │ ├── .gitignore │ ├── AUTHORS.rst │ ├── CONTRIBUTING.rst │ ├── HISTORY.rst │ ├── LICENSE │ ├── MANIFEST.in │ ├── Makefile │ ├── README.rst │ ├── docs │ ├── Makefile │ ├── api.rst │ ├── authors.rst │ ├── conf.py │ ├── contributing.rst │ ├── history.rst │ ├── index.rst │ ├── installation.rst │ ├── make.bat │ ├── readme.rst │ └── usage.rst │ ├── requirements_dev.in │ ├── requirements_dev.txt │ ├── setup.cfg │ ├── setup.py │ ├── tests │ ├── __init__.py │ └── test_uptimer.py │ ├── tox.ini │ └── uptimer │ ├── __init__.py │ ├── cli.py │ └── helpers.py ├── ch13-executable ├── gui.exe ├── guptimer │ ├── __init__.py │ ├── gui.py │ └── helpers.py └── setup.py ├── ch14-deployment ├── .vscode │ └── settings.json ├── Dockerfile ├── Procfile ├── helpers.py ├── main.py ├── requirements.in ├── requirements.txt └── templates │ └── home.html ├── guides └── pyenv_vscode │ ├── README.md │ └── resources │ ├── 1-experiments-opt-into.png │ ├── 2-pythondiscoverymodule.png │ ├── 3-python-select-interpreter.png │ ├── 4-environment-variables.png │ └── 5-i-can-t-find-the-interpreter.png └── readme_resources └── course-img.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Talk Python 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Modern Python Projects Course](https://talkpython.fm/modern-python-projects) 2 | 3 | [![Course image](./readme_resources/course-img.jpg)](https://talkpython.fm/modern-python-projects) 4 | 5 | Modern Python Projects is a course that bridges the gap between "knowing Python" and "comfortably building Python applications." You will learn everything you need to know to set up a robust development environment, add helpful tools, and build typical Python projects. 6 | 7 | ## What you will learn 8 | 9 | Modern Python Projects covers the following topics: 10 | 11 | * Setting up VS Code for Python development - how to configure it, debug your code, run tests, and what plugins to install. 12 | * Managing Python versions and packages, isolating dependencies between projects, and installing global packages. 13 | * Using cookiecutter for a head start when creating a new project. 14 | * Managing Python projects - how to write good requirements files, pin dependencies with pip-tools, manage tasks with Makefiles, and use Poetry to do all that at once. 15 | * Adding formatters, linters, and other static analysis tools to make your code better. 16 | * Testing your code with pytest - how to configure it, how to use fixtures, mocks, or parametrization, and what plugins to install. 17 | * Documenting your code with Sphinx, automatically extracting the documentation from the code, and testing code examples. 18 | * How to automate some typical tasks with tox and pre-commit. And how to do the same inside your code repository using continuous integration tools like GitHub Actions or GitLab CI. 19 | * How to deploy your application using a Platform as a Service or Docker. 20 | 21 | That's a lot of knowledge, so to put it into practice, we will build three projects on the way: 22 | 23 | * A CLI application that you can run in your terminal. 24 | * A Python package that we will publish on [PyPI](https://pypi.org/). 25 | * An executable application that you can send to someone, and they will be able to run it without even having Python installed on their computer. 26 | 27 | You can find all the additional links mentioned throughout this course at [modernpythonprojects.com/resources](https://modernpythonprojects.com/resources/). 28 | 29 | ## Who is this course for? 30 | 31 | This course is for people who already know Python's basics, but who still struggle with using it in everyday work. If installing Python packages breaks things on your computer, you're not sure how to start a new Python projects, how to add tests and documentation, or what's the best way to deploy it - this course is for you. 32 | 33 | In a few examples, I use very simple Flask and FastAPI websites, but it's just a few lines of code, and I explain everything. 34 | 35 | ## Sounds interesting? 36 | 37 | [Take the course today at Talk Python Training](https://talkpython.fm/modern-python-projects). 38 | -------------------------------------------------------------------------------- /ch03-code-editor/app01.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | app = Flask(__name__) 3 | 4 | @app.route("/") 5 | def home(): 6 | return "Hello from Flask!" 7 | -------------------------------------------------------------------------------- /ch03-code-editor/app02.py: -------------------------------------------------------------------------------- 1 | import random 2 | from flask import Flask 3 | app = Flask(__name__) 4 | 5 | surname = "Witowski" 6 | 7 | @app.route("/") 8 | def home(): 9 | name = "Sebastian" 10 | lucky = random.randint(0, 100) 11 | 12 | return f"Hello {name}! Your lucky number is: {lucky}." 13 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | *.pyc 3 | __pycache__/ 4 | instance/ 5 | .cache/ 6 | .pytest_cache/ 7 | .coverage 8 | htmlcov/ 9 | dist/ 10 | build/ 11 | *.egg-info/ 12 | .idea/ 13 | *.swp 14 | *~ 15 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/LICENSE.rst: -------------------------------------------------------------------------------- 1 | Copyright 2010 Pallets 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.rst 2 | include flaskr/schema.sql 3 | graft flaskr/static 4 | graft flaskr/templates 5 | graft tests 6 | global-exclude *.pyc 7 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/README.rst: -------------------------------------------------------------------------------- 1 | Flaskr 2 | ====== 3 | 4 | The basic blog app built in the Flask `tutorial`_. 5 | 6 | .. _tutorial: https://flask.palletsprojects.com/tutorial/ 7 | 8 | 9 | Install 10 | ------- 11 | 12 | **Be sure to use the same version of the code as the version of the docs 13 | you're reading.** You probably want the latest tagged version, but the 14 | default Git version is the master branch. :: 15 | 16 | # clone the repository 17 | $ git clone https://github.com/pallets/flask 18 | $ cd flask 19 | # checkout the correct version 20 | $ git tag # shows the tagged versions 21 | $ git checkout latest-tag-found-above 22 | $ cd examples/tutorial 23 | 24 | Create a virtualenv and activate it:: 25 | 26 | $ python3 -m venv venv 27 | $ . venv/bin/activate 28 | 29 | Or on Windows cmd:: 30 | 31 | $ py -3 -m venv venv 32 | $ venv\Scripts\activate.bat 33 | 34 | Install Flaskr:: 35 | 36 | $ pip install -e . 37 | 38 | Or if you are using the master branch, install Flask from source before 39 | installing Flaskr:: 40 | 41 | $ pip install -e ../.. 42 | $ pip install -e . 43 | 44 | 45 | Run 46 | --- 47 | 48 | :: 49 | 50 | $ export FLASK_APP=flaskr 51 | $ export FLASK_ENV=development 52 | $ flask init-db 53 | $ flask run 54 | 55 | Or on Windows cmd:: 56 | 57 | > set FLASK_APP=flaskr 58 | > set FLASK_ENV=development 59 | > flask init-db 60 | > flask run 61 | 62 | Open http://127.0.0.1:5000 in a browser. 63 | 64 | 65 | Test 66 | ---- 67 | 68 | :: 69 | 70 | $ pip install '.[test]' 71 | $ pytest 72 | 73 | Run with coverage report:: 74 | 75 | $ coverage run -m pytest 76 | $ coverage report 77 | $ coverage html # open htmlcov/index.html in a browser 78 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/flaskr/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask 4 | 5 | 6 | def create_app(test_config=None): 7 | """Create and configure an instance of the Flask application.""" 8 | app = Flask(__name__, instance_relative_config=True) 9 | app.config.from_mapping( 10 | # a default secret that should be overridden by instance config 11 | SECRET_KEY="dev", 12 | # store the database in the instance folder 13 | DATABASE=os.path.join(app.instance_path, "flaskr.sqlite"), 14 | ) 15 | 16 | if test_config is None: 17 | # load the instance config, if it exists, when not testing 18 | app.config.from_pyfile("config.py", silent=True) 19 | else: 20 | # load the test config if passed in 21 | app.config.update(test_config) 22 | 23 | # ensure the instance folder exists 24 | try: 25 | os.makedirs(app.instance_path) 26 | except OSError: 27 | pass 28 | 29 | @app.route("/hello") 30 | def hello(): 31 | return "Hello, World!" 32 | 33 | # register the database commands 34 | from flaskr import db 35 | 36 | db.init_app(app) 37 | 38 | # apply the blueprints to the app 39 | from flaskr import auth, blog 40 | 41 | app.register_blueprint(auth.bp) 42 | app.register_blueprint(blog.bp) 43 | 44 | # make url_for('index') == url_for('blog.index') 45 | # in another app, you might define a separate main index here with 46 | # app.route, while giving the blog blueprint a url_prefix, but for 47 | # the tutorial the blog will be the main index 48 | app.add_url_rule("/", endpoint="index") 49 | 50 | return app 51 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/flaskr/auth.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from flask import Blueprint 4 | from flask import flash 5 | from flask import g 6 | from flask import redirect 7 | from flask import render_template 8 | from flask import request 9 | from flask import session 10 | from flask import url_for 11 | from werkzeug.security import check_password_hash 12 | from werkzeug.security import generate_password_hash 13 | 14 | from flaskr.db import get_db 15 | 16 | bp = Blueprint("auth", __name__, url_prefix="/auth") 17 | 18 | 19 | def login_required(view): 20 | """View decorator that redirects anonymous users to the login page.""" 21 | 22 | @functools.wraps(view) 23 | def wrapped_view(**kwargs): 24 | if g.user is None: 25 | return redirect(url_for("auth.login")) 26 | 27 | return view(**kwargs) 28 | 29 | return wrapped_view 30 | 31 | 32 | @bp.before_app_request 33 | def load_logged_in_user(): 34 | """If a user id is stored in the session, load the user object from 35 | the database into ``g.user``.""" 36 | user_id = session.get("user_id") 37 | 38 | if user_id is None: 39 | g.user = None 40 | else: 41 | g.user = ( 42 | get_db().execute("SELECT * FROM user WHERE id = ?", (user_id,)).fetchone() 43 | ) 44 | 45 | 46 | @bp.route("/register", methods=("GET", "POST")) 47 | def register(): 48 | """Register a new user. 49 | 50 | Validates that the username is not already taken. Hashes the 51 | password for security. 52 | """ 53 | if request.method == "POST": 54 | username = request.form["username"] 55 | password = request.form["password"] 56 | db = get_db() 57 | error = None 58 | 59 | if not username: 60 | error = "Username is required." 61 | elif not password: 62 | error = "Password is required." 63 | elif ( 64 | db.execute("SELECT id FROM user WHERE username = ?", (username,)).fetchone() 65 | is not None 66 | ): 67 | error = f"User {username} is already registered." 68 | 69 | if error is None: 70 | # the name is available, store it in the database and go to 71 | # the login page 72 | db.execute( 73 | "INSERT INTO user (username, password) VALUES (?, ?)", 74 | (username, generate_password_hash(password)), 75 | ) 76 | db.commit() 77 | return redirect(url_for("auth.login")) 78 | 79 | flash(error) 80 | 81 | return render_template("auth/register.html") 82 | 83 | 84 | @bp.route("/login", methods=("GET", "POST")) 85 | def login(): 86 | """Log in a registered user by adding the user id to the session.""" 87 | if request.method == "POST": 88 | username = request.form["username"] 89 | password = request.form["password"] 90 | db = get_db() 91 | error = None 92 | user = db.execute( 93 | "SELECT * FROM user WHERE username = ?", (username,) 94 | ).fetchone() 95 | 96 | if user is None: 97 | error = "Incorrect username." 98 | elif not check_password_hash(user["password"], password): 99 | error = "Incorrect password." 100 | 101 | if error is None: 102 | # store the user id in a new session and return to the index 103 | session.clear() 104 | session["user_id"] = user["id"] 105 | return redirect(url_for("index")) 106 | 107 | flash(error) 108 | 109 | return render_template("auth/login.html") 110 | 111 | 112 | @bp.route("/logout") 113 | def logout(): 114 | """Clear the current session, including the stored user id.""" 115 | session.clear() 116 | return redirect(url_for("index")) 117 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/flaskr/blog.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from flask import flash 3 | from flask import g 4 | from flask import redirect 5 | from flask import render_template 6 | from flask import request 7 | from flask import url_for 8 | from werkzeug.exceptions import abort 9 | 10 | from flaskr.auth import login_required 11 | from flaskr.db import get_db 12 | 13 | bp = Blueprint("blog", __name__) 14 | 15 | 16 | @bp.route("/") 17 | def index(): 18 | """Show all the posts, most recent first.""" 19 | db = get_db() 20 | posts = db.execute( 21 | "SELECT p.id, title, body, created, author_id, username" 22 | " FROM post p JOIN user u ON p.author_id = u.id" 23 | " ORDER BY created DESC" 24 | ).fetchall() 25 | return render_template("blog/index.html", posts=posts) 26 | 27 | 28 | def get_post(id, check_author=True): 29 | """Get a post and its author by id. 30 | 31 | Checks that the id exists and optionally that the current user is 32 | the author. 33 | 34 | :param id: id of post to get 35 | :param check_author: require the current user to be the author 36 | :return: the post with author information 37 | :raise 404: if a post with the given id doesn't exist 38 | :raise 403: if the current user isn't the author 39 | """ 40 | post = ( 41 | get_db() 42 | .execute( 43 | "SELECT p.id, title, body, created, author_id, username" 44 | " FROM post p JOIN user u ON p.author_id = u.id" 45 | " WHERE p.id = ?", 46 | (id,), 47 | ) 48 | .fetchone() 49 | ) 50 | 51 | if post is None: 52 | abort(404, f"Post id {id} doesn't exist.") 53 | 54 | if check_author and post["author_id"] != g.user["id"]: 55 | abort(403) 56 | 57 | return post 58 | 59 | 60 | @bp.route("/create", methods=("GET", "POST")) 61 | @login_required 62 | def create(): 63 | """Create a new post for the current user.""" 64 | if request.method == "POST": 65 | title = request.form["title"] 66 | body = request.form["body"] 67 | error = None 68 | 69 | if not title: 70 | error = "Title is required." 71 | 72 | if error is not None: 73 | flash(error) 74 | else: 75 | db = get_db() 76 | db.execute( 77 | "INSERT INTO post (title, body, author_id) VALUES (?, ?, ?)", 78 | (title, body, g.user["id"]), 79 | ) 80 | db.commit() 81 | return redirect(url_for("blog.index")) 82 | 83 | return render_template("blog/create.html") 84 | 85 | 86 | @bp.route("//update", methods=("GET", "POST")) 87 | @login_required 88 | def update(id): 89 | """Update a post if the current user is the author.""" 90 | post = get_post(id) 91 | 92 | if request.method == "POST": 93 | title = request.form["title"] 94 | body = request.form["body"] 95 | error = None 96 | 97 | if not title: 98 | error = "Title is required." 99 | 100 | if error is not None: 101 | flash(error) 102 | else: 103 | db = get_db() 104 | db.execute( 105 | "UPDATE post SET title = ?, body = ? WHERE id = ?", (title, body, id) 106 | ) 107 | db.commit() 108 | return redirect(url_for("blog.index")) 109 | 110 | return render_template("blog/update.html", post=post) 111 | 112 | 113 | @bp.route("//delete", methods=("POST",)) 114 | @login_required 115 | def delete(id): 116 | """Delete a post. 117 | 118 | Ensures that the post exists and that the logged in user is the 119 | author of the post. 120 | """ 121 | get_post(id) 122 | db = get_db() 123 | db.execute("DELETE FROM post WHERE id = ?", (id,)) 124 | db.commit() 125 | return redirect(url_for("blog.index")) 126 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/flaskr/db.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | import click 4 | from flask import current_app 5 | from flask import g 6 | from flask.cli import with_appcontext 7 | 8 | 9 | def get_db(): 10 | """Connect to the application's configured database. The connection 11 | is unique for each request and will be reused if this is called 12 | again. 13 | """ 14 | if "db" not in g: 15 | g.db = sqlite3.connect( 16 | current_app.config["DATABASE"], detect_types=sqlite3.PARSE_DECLTYPES 17 | ) 18 | g.db.row_factory = sqlite3.Row 19 | 20 | return g.db 21 | 22 | 23 | def close_db(e=None): 24 | """If this request connected to the database, close the 25 | connection. 26 | """ 27 | db = g.pop("db", None) 28 | 29 | if db is not None: 30 | db.close() 31 | 32 | 33 | def init_db(): 34 | """Clear existing data and create new tables.""" 35 | db = get_db() 36 | 37 | with current_app.open_resource("schema.sql") as f: 38 | db.executescript(f.read().decode("utf8")) 39 | 40 | 41 | @click.command("init-db") 42 | @with_appcontext 43 | def init_db_command(): 44 | """Clear existing data and create new tables.""" 45 | init_db() 46 | click.echo("Initialized the database.") 47 | 48 | 49 | def init_app(app): 50 | """Register database functions with the Flask app. This is called by 51 | the application factory. 52 | """ 53 | app.teardown_appcontext(close_db) 54 | app.cli.add_command(init_db_command) 55 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/flaskr/schema.sql: -------------------------------------------------------------------------------- 1 | -- Initialize the database. 2 | -- Drop any existing data and create empty tables. 3 | 4 | DROP TABLE IF EXISTS user; 5 | DROP TABLE IF EXISTS post; 6 | 7 | CREATE TABLE user ( 8 | id INTEGER PRIMARY KEY AUTOINCREMENT, 9 | username TEXT UNIQUE NOT NULL, 10 | password TEXT NOT NULL 11 | ); 12 | 13 | CREATE TABLE post ( 14 | id INTEGER PRIMARY KEY AUTOINCREMENT, 15 | author_id INTEGER NOT NULL, 16 | created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | title TEXT NOT NULL, 18 | body TEXT NOT NULL, 19 | FOREIGN KEY (author_id) REFERENCES user (id) 20 | ); 21 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/flaskr/static/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | background: #eee; 4 | padding: 1rem; 5 | } 6 | 7 | body { 8 | max-width: 960px; 9 | margin: 0 auto; 10 | background: white; 11 | } 12 | 13 | h1, h2, h3, h4, h5, h6 { 14 | font-family: serif; 15 | color: #377ba8; 16 | margin: 1rem 0; 17 | } 18 | 19 | a { 20 | color: #377ba8; 21 | } 22 | 23 | hr { 24 | border: none; 25 | border-top: 1px solid lightgray; 26 | } 27 | 28 | nav { 29 | background: lightgray; 30 | display: flex; 31 | align-items: center; 32 | padding: 0 0.5rem; 33 | } 34 | 35 | nav h1 { 36 | flex: auto; 37 | margin: 0; 38 | } 39 | 40 | nav h1 a { 41 | text-decoration: none; 42 | padding: 0.25rem 0.5rem; 43 | } 44 | 45 | nav ul { 46 | display: flex; 47 | list-style: none; 48 | margin: 0; 49 | padding: 0; 50 | } 51 | 52 | nav ul li a, nav ul li span, header .action { 53 | display: block; 54 | padding: 0.5rem; 55 | } 56 | 57 | .content { 58 | padding: 0 1rem 1rem; 59 | } 60 | 61 | .content > header { 62 | border-bottom: 1px solid lightgray; 63 | display: flex; 64 | align-items: flex-end; 65 | } 66 | 67 | .content > header h1 { 68 | flex: auto; 69 | margin: 1rem 0 0.25rem 0; 70 | } 71 | 72 | .flash { 73 | margin: 1em 0; 74 | padding: 1em; 75 | background: #cae6f6; 76 | border: 1px solid #377ba8; 77 | } 78 | 79 | .post > header { 80 | display: flex; 81 | align-items: flex-end; 82 | font-size: 0.85em; 83 | } 84 | 85 | .post > header > div:first-of-type { 86 | flex: auto; 87 | } 88 | 89 | .post > header h1 { 90 | font-size: 1.5em; 91 | margin-bottom: 0; 92 | } 93 | 94 | .post .about { 95 | color: slategray; 96 | font-style: italic; 97 | } 98 | 99 | .post .body { 100 | white-space: pre-line; 101 | } 102 | 103 | .content:last-child { 104 | margin-bottom: 0; 105 | } 106 | 107 | .content form { 108 | margin: 1em 0; 109 | display: flex; 110 | flex-direction: column; 111 | } 112 | 113 | .content label { 114 | font-weight: bold; 115 | margin-bottom: 0.5em; 116 | } 117 | 118 | .content input, .content textarea { 119 | margin-bottom: 1em; 120 | } 121 | 122 | .content textarea { 123 | min-height: 12em; 124 | resize: vertical; 125 | } 126 | 127 | input.danger { 128 | color: #cc2f2e; 129 | } 130 | 131 | input[type=submit] { 132 | align-self: start; 133 | min-width: 10em; 134 | } 135 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/flaskr/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Log In{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/flaskr/templates/auth/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Register{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/flaskr/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% block title %}{% endblock %} - Flaskr 3 | 4 | 16 |
17 |
18 | {% block header %}{% endblock %} 19 |
20 | {% for message in get_flashed_messages() %} 21 |
{{ message }}
22 | {% endfor %} 23 | {% block content %}{% endblock %} 24 |
25 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/flaskr/templates/blog/create.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}New Post{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/flaskr/templates/blog/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Posts{% endblock %}

5 | {% if g.user %} 6 | New 7 | {% endif %} 8 | {% endblock %} 9 | 10 | {% block content %} 11 | {% for post in posts %} 12 |
13 |
14 |
15 |

{{ post['title'] }}

16 |
by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}
17 |
18 | {% if g.user['id'] == post['author_id'] %} 19 | Edit 20 | {% endif %} 21 |
22 |

{{ post['body'] }}

23 |
24 | {% if not loop.last %} 25 |
26 | {% endif %} 27 | {% endfor %} 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/flaskr/templates/blog/update.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = flaskr 3 | version = 1.0.0 4 | url = https://flask.palletsprojects.com/tutorial/ 5 | license = BSD-3-Clause 6 | maintainer = Pallets 7 | maintainer_email = contact@palletsprojects.com 8 | description = The basic blog app built in the Flask tutorial. 9 | long_description = file: README.rst 10 | long_description_content_type = text/x-rst 11 | 12 | [options] 13 | packages = find: 14 | include_package_data = true 15 | install_requires = 16 | Flask 17 | 18 | [options.extras_require] 19 | test = 20 | pytest 21 | 22 | [tool:pytest] 23 | testpaths = tests 24 | 25 | [coverage:run] 26 | branch = True 27 | source = 28 | flaskr 29 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | 4 | import pytest 5 | 6 | from flaskr import create_app 7 | from flaskr.db import get_db 8 | from flaskr.db import init_db 9 | 10 | # read in SQL for populating test data 11 | with open(os.path.join(os.path.dirname(__file__), "data.sql"), "rb") as f: 12 | _data_sql = f.read().decode("utf8") 13 | 14 | 15 | @pytest.fixture 16 | def app(): 17 | """Create and configure a new app instance for each test.""" 18 | # create a temporary file to isolate the database for each test 19 | db_fd, db_path = tempfile.mkstemp() 20 | # create the app with common test config 21 | app = create_app({"TESTING": True, "DATABASE": db_path}) 22 | 23 | # create the database and load test data 24 | with app.app_context(): 25 | init_db() 26 | get_db().executescript(_data_sql) 27 | 28 | yield app 29 | 30 | # close and remove the temporary database 31 | os.close(db_fd) 32 | os.unlink(db_path) 33 | 34 | 35 | @pytest.fixture 36 | def client(app): 37 | """A test client for the app.""" 38 | return app.test_client() 39 | 40 | 41 | @pytest.fixture 42 | def runner(app): 43 | """A test runner for the app's Click commands.""" 44 | return app.test_cli_runner() 45 | 46 | 47 | class AuthActions: 48 | def __init__(self, client): 49 | self._client = client 50 | 51 | def login(self, username="test", password="test"): 52 | return self._client.post( 53 | "/auth/login", data={"username": username, "password": password} 54 | ) 55 | 56 | def logout(self): 57 | return self._client.get("/auth/logout") 58 | 59 | 60 | @pytest.fixture 61 | def auth(client): 62 | return AuthActions(client) 63 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/tests/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO user (username, password) 2 | VALUES 3 | ('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'), 4 | ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79'); 5 | 6 | INSERT INTO post (title, body, author_id, created) 7 | VALUES 8 | ('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00'); 9 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from flask import g 3 | from flask import session 4 | 5 | from flaskr.db import get_db 6 | 7 | 8 | def test_register(client, app): 9 | # test that viewing the page renders without template errors 10 | assert client.get("/auth/register").status_code == 200 11 | 12 | # test that successful registration redirects to the login page 13 | response = client.post("/auth/register", data={"username": "a", "password": "a"}) 14 | assert "http://localhost/auth/login" == response.headers["Location"] 15 | 16 | # test that the user was inserted into the database 17 | with app.app_context(): 18 | assert ( 19 | get_db().execute("select * from user where username = 'a'").fetchone() 20 | is not None 21 | ) 22 | 23 | 24 | @pytest.mark.parametrize( 25 | ("username", "password", "message"), 26 | ( 27 | ("", "", b"Username is required."), 28 | ("a", "", b"Password is required."), 29 | ("test", "test", b"already registered"), 30 | ), 31 | ) 32 | def test_register_validate_input(client, username, password, message): 33 | response = client.post( 34 | "/auth/register", data={"username": username, "password": password} 35 | ) 36 | assert message in response.data 37 | 38 | 39 | def test_login(client, auth): 40 | # test that viewing the page renders without template errors 41 | assert client.get("/auth/login").status_code == 200 42 | 43 | # test that successful login redirects to the index page 44 | response = auth.login() 45 | assert response.headers["Location"] == "http://localhost/" 46 | 47 | # login request set the user_id in the session 48 | # check that the user is loaded from the session 49 | with client: 50 | client.get("/") 51 | assert session["user_id"] == 1 52 | assert g.user["username"] == "test" 53 | 54 | 55 | @pytest.mark.parametrize( 56 | ("username", "password", "message"), 57 | (("a", "test", b"Incorrect username."), ("test", "a", b"Incorrect password.")), 58 | ) 59 | def test_login_validate_input(auth, username, password, message): 60 | response = auth.login(username, password) 61 | assert message in response.data 62 | 63 | 64 | def test_logout(client, auth): 65 | auth.login() 66 | 67 | with client: 68 | auth.logout() 69 | assert "user_id" not in session 70 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/tests/test_blog.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flaskr.db import get_db 4 | 5 | 6 | def test_index(client, auth): 7 | response = client.get("/") 8 | assert b"Log In" in response.data 9 | assert b"Register" in response.data 10 | 11 | auth.login() 12 | response = client.get("/") 13 | assert b"test title" in response.data 14 | assert b"by test on 2018-01-01" in response.data 15 | assert b"test\nbody" in response.data 16 | assert b'href="/1/update"' in response.data 17 | 18 | 19 | @pytest.mark.parametrize("path", ("/create", "/1/update", "/1/delete")) 20 | def test_login_required(client, path): 21 | response = client.post(path) 22 | assert response.headers["Location"] == "http://localhost/auth/login" 23 | 24 | 25 | def test_author_required(app, client, auth): 26 | # change the post author to another user 27 | with app.app_context(): 28 | db = get_db() 29 | db.execute("UPDATE post SET author_id = 2 WHERE id = 1") 30 | db.commit() 31 | 32 | auth.login() 33 | # current user can't modify other user's post 34 | assert client.post("/1/update").status_code == 403 35 | assert client.post("/1/delete").status_code == 403 36 | # current user doesn't see edit link 37 | assert b'href="/1/update"' not in client.get("/").data 38 | 39 | 40 | @pytest.mark.parametrize("path", ("/2/update", "/2/delete")) 41 | def test_exists_required(client, auth, path): 42 | auth.login() 43 | assert client.post(path).status_code == 404 44 | 45 | 46 | def test_create(client, auth, app): 47 | auth.login() 48 | assert client.get("/create").status_code == 200 49 | client.post("/create", data={"title": "created", "body": ""}) 50 | 51 | with app.app_context(): 52 | db = get_db() 53 | count = db.execute("SELECT COUNT(id) FROM post").fetchone()[0] 54 | assert count == 2 55 | 56 | 57 | def test_update(client, auth, app): 58 | auth.login() 59 | assert client.get("/1/update").status_code == 200 60 | client.post("/1/update", data={"title": "updated", "body": ""}) 61 | 62 | with app.app_context(): 63 | db = get_db() 64 | post = db.execute("SELECT * FROM post WHERE id = 1").fetchone() 65 | assert post["title"] == "updated" 66 | 67 | 68 | @pytest.mark.parametrize("path", ("/create", "/1/update")) 69 | def test_create_update_validate(client, auth, path): 70 | auth.login() 71 | response = client.post(path, data={"title": "", "body": ""}) 72 | assert b"Title is required." in response.data 73 | 74 | 75 | def test_delete(client, auth, app): 76 | auth.login() 77 | response = client.post("/1/delete") 78 | assert response.headers["Location"] == "http://localhost/" 79 | 80 | with app.app_context(): 81 | db = get_db() 82 | post = db.execute("SELECT * FROM post WHERE id = 1").fetchone() 83 | assert post is None 84 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/tests/test_db.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | import pytest 4 | 5 | from flaskr.db import get_db 6 | 7 | 8 | def test_get_close_db(app): 9 | with app.app_context(): 10 | db = get_db() 11 | assert db is get_db() 12 | 13 | with pytest.raises(sqlite3.ProgrammingError) as e: 14 | db.execute("SELECT 1") 15 | 16 | assert "closed" in str(e.value) 17 | 18 | 19 | def test_init_db_command(runner, monkeypatch): 20 | class Recorder: 21 | called = False 22 | 23 | def fake_init_db(): 24 | Recorder.called = True 25 | 26 | monkeypatch.setattr("flaskr.db.init_db", fake_init_db) 27 | result = runner.invoke(args=["init-db"]) 28 | assert "Initialized" in result.output 29 | assert Recorder.called 30 | -------------------------------------------------------------------------------- /ch06-managing-projects/advanced/tests/test_factory.py: -------------------------------------------------------------------------------- 1 | from flaskr import create_app 2 | 3 | 4 | def test_config(): 5 | """Test create_app without passing test config.""" 6 | assert not create_app().testing 7 | assert create_app({"TESTING": True}).testing 8 | 9 | 10 | def test_hello(client): 11 | response = client.get("/hello") 12 | assert response.data == b"Hello, World!" 13 | -------------------------------------------------------------------------------- /ch06-managing-projects/medium/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sebastian Witowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ch06-managing-projects/medium/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: init build run unittest test 2 | 3 | TODO: Update this makefile and remove unused stuff 4 | help: 5 | @echo "init - set up the application" 6 | @echo "build - build Docker containers" 7 | @echo "run - start Docker containers" 8 | @echo "test - run all tests" 9 | @echo "unittest - run unit tests only" 10 | 11 | 12 | init: 13 | make build 14 | make run 15 | docker-compose exec web simple db upgrade 16 | docker-compose exec web simple db migrate 17 | 18 | build: 19 | docker-compose build 20 | 21 | run: 22 | docker-compose up -d 23 | 24 | unittest: 25 | pytest -vv -m unit --no-cov 26 | 27 | test: 28 | pytest tests 29 | 30 | -------------------------------------------------------------------------------- /ch06-managing-projects/medium/README.md: -------------------------------------------------------------------------------- 1 | # simple project 2 | 3 | This is a very simple project with only one Python file. 4 | 5 | ## How to run it? 6 | 7 | ``` 8 | python simple.py 9 | ``` 10 | 11 | -------------------------------------------------------------------------------- /ch06-managing-projects/medium/main.py: -------------------------------------------------------------------------------- 1 | from medium.db import connect, get_users 2 | from medium.utils import list_users 3 | 4 | 5 | def main(): 6 | # Connect to a DB 7 | connect() 8 | # Print users' information 9 | users = get_users() 10 | print(list_users(users)) 11 | 12 | 13 | if __name__ == "__main__": 14 | main() 15 | -------------------------------------------------------------------------------- /ch06-managing-projects/medium/medium/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/ch06-managing-projects/medium/medium/__init__.py -------------------------------------------------------------------------------- /ch06-managing-projects/medium/medium/db.py: -------------------------------------------------------------------------------- 1 | def connect(*args, **kwargs): 2 | # Here goes some code to connect to a DB 3 | print("Successfully connected") 4 | 5 | 6 | def get_users(): 7 | # Mock reading users from a DB table 8 | return [ 9 | {"name": "Alice", "city": "San Francisco"}, 10 | {"name": "Bob", "city": "Chicago"}, 11 | ] 12 | -------------------------------------------------------------------------------- /ch06-managing-projects/medium/medium/medium.py: -------------------------------------------------------------------------------- 1 | from medium.db import connect, get_users 2 | from medium.utils import list_users 3 | 4 | 5 | def main(): 6 | # Connect to a DB 7 | connect() 8 | # Print users' information 9 | users = get_users() 10 | print(list_users(users)) 11 | 12 | 13 | if __name__ == "__main__": 14 | main() 15 | -------------------------------------------------------------------------------- /ch06-managing-projects/medium/medium/utils.py: -------------------------------------------------------------------------------- 1 | def list_users(users): 2 | return "Users: \n* " + "\n* ".join([f"{user['name']} from {user['city']}" for user in users]) 3 | 4 | -------------------------------------------------------------------------------- /ch06-managing-projects/medium/requirements.txt: -------------------------------------------------------------------------------- 1 | # requirements.txt 2 | 3 | Django==3.1.14 4 | pytest 5 | git+git://github.com/jazzband/django-redis@master#egg=django-redis 6 | -------------------------------------------------------------------------------- /ch06-managing-projects/medium/tests/test_db.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/ch06-managing-projects/medium/tests/test_db.py -------------------------------------------------------------------------------- /ch06-managing-projects/medium/tests/test_medium.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/ch06-managing-projects/medium/tests/test_medium.py -------------------------------------------------------------------------------- /ch06-managing-projects/medium/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from simple import main 2 | 3 | 4 | def test_main_function(): 5 | assert main() == "Hi, this is a very simple Python project - only one file!" 6 | -------------------------------------------------------------------------------- /ch06-managing-projects/simple/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sebastian Witowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ch06-managing-projects/simple/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: init build run unittest test 2 | 3 | TODO: Update this makefile and remove unused stuff 4 | help: 5 | @echo "init - set up the application" 6 | @echo "build - build Docker containers" 7 | @echo "run - start Docker containers" 8 | @echo "test - run all tests" 9 | @echo "unittest - run unit tests only" 10 | 11 | 12 | init: 13 | make build 14 | make run 15 | docker-compose exec web simple db upgrade 16 | docker-compose exec web simple db migrate 17 | 18 | build: 19 | docker-compose build 20 | 21 | run: 22 | docker-compose up -d 23 | 24 | unittest: 25 | pytest -vv -m unit --no-cov 26 | 27 | test: 28 | pytest tests 29 | -------------------------------------------------------------------------------- /ch06-managing-projects/simple/README.md: -------------------------------------------------------------------------------- 1 | # simple project 2 | 3 | This is a very simple project with only one Python file. 4 | 5 | ## How to run it? 6 | 7 | ``` 8 | python simple.py 9 | ``` 10 | 11 | -------------------------------------------------------------------------------- /ch06-managing-projects/simple/requirements.txt: -------------------------------------------------------------------------------- 1 | # Here you can specify the requirements for your project 2 | 3 | Django==3.1.14 4 | pytest 5 | -------------------------------------------------------------------------------- /ch06-managing-projects/simple/simple.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | return "Hi, this is a very simple Python project - only one file!" 3 | 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /ch06-managing-projects/simple/tests.py: -------------------------------------------------------------------------------- 1 | from simple import main 2 | 3 | 4 | def test_main_function(): 5 | assert main() == "Hi, this is a very simple Python project - only one file!" 6 | -------------------------------------------------------------------------------- /ch08-testing/pyproject.toml: -------------------------------------------------------------------------------- 1 | # pyproject.toml 2 | [tool.pytest.ini_options] 3 | minversion = "6.0" 4 | # addopts = "-ra -q -m 'not slow'" 5 | testpaths = [ 6 | "tests", 7 | "integration", 8 | ] -------------------------------------------------------------------------------- /ch08-testing/pytest_docstring.py: -------------------------------------------------------------------------------- 1 | def add_two_numbers(a, b): 2 | """ Adds two numbers 3 | >>> add_two_numbers(2, 2) 4 | 5 5 | """ 6 | return a + b 7 | -------------------------------------------------------------------------------- /ch08-testing/pytest_example.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class TestStringMethods: 5 | def test_upper(self): 6 | assert "foo".upper() == "FOO" 7 | 8 | def test_isupper(self): 9 | assert "foo".isupper() is True, "Foo is not upper!" 10 | assert "Foo".isupper() is False 11 | 12 | # @pytest.mark.slow 13 | def test_split(self): 14 | s = "hello world" 15 | assert s.split() == ["hello", "world"] 16 | with pytest.raises(TypeError): 17 | s.split(2) 18 | -------------------------------------------------------------------------------- /ch08-testing/pytest_fixtures.py: -------------------------------------------------------------------------------- 1 | # THIS IS A DUMMY CODE THAT WON'T WORK WHEN YOU RUN IT! 2 | 3 | import pytest 4 | 5 | @pytest.fixture 6 | def authenticated_user(): 7 | user = create_user(email="user@test.com", password="1234") 8 | authenticate(email="user@test.com", password="1234") 9 | return user 10 | 11 | 12 | def test_buy_item(authenticated_user): 13 | buy_item(authenticated_user, item="Book") 14 | # ... the rest of the test 15 | 16 | def test_admin_permissions(authenticated_user): 17 | # User can access home page 18 | assert can_access(authenticated_user, "/") is True 19 | # User can't access the admin page 20 | assert can_access(authenticated_user, "admin/") is False 21 | -------------------------------------------------------------------------------- /ch08-testing/pytest_mocking.py: -------------------------------------------------------------------------------- 1 | # THIS IS A DUMMY CODE THAT WON'T WORK WHEN YOU RUN IT! 2 | 3 | # Function 4 | def charge_customer(amount): 5 | response = Stripe.charge(amount) 6 | 7 | if response.get('status') == "success": 8 | current_order_status = "processing" 9 | else: 10 | display_error_message(response.get('error')) 11 | 12 | # Test 13 | def test_payment(monkeypatch): 14 | # Patch the Stripe.charge() method and make it return "success" status 15 | monkeypatch.setattr(Stripe, "charge", dict(status="success")) 16 | 17 | # This calls our monkeypatch that always returns "success" 18 | charge_customer("199") 19 | assert current_order_status == "processing" 20 | # ... and so on 21 | -------------------------------------------------------------------------------- /ch08-testing/pytest_parametrize.py: -------------------------------------------------------------------------------- 1 | # THIS IS A DUMMY CODE THAT WON'T WORK WHEN YOU RUN IT! 2 | 3 | import pytest 4 | 5 | @pytest.mark.parametrize( 6 | "number_of_items,expected_cart_size", 7 | [ 8 | (1, 1), 9 | (10, 10), 10 | (-1, 0), 11 | ] 12 | ) 13 | def test_add_items_to_cart(number_of_items,expected_cart_size): 14 | cart = Cart() 15 | cart.add_item("Book", number_of_items=number_of_items) 16 | assert cart.size == expected_cart_size 17 | -------------------------------------------------------------------------------- /ch08-testing/pytest_parametrize_calculator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | @pytest.mark.parametrize( 4 | "left,right,output", 5 | [ 6 | (1, 1, 2), 7 | (10, 100, 110), 8 | (1, -10, -9), 9 | (-10, -10, -10), 10 | ] 11 | ) 12 | def test_add_numbers(left, right, output): 13 | assert left + right == output 14 | -------------------------------------------------------------------------------- /ch08-testing/test_string_methods.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | class TestStringMethods(unittest.TestCase): 4 | 5 | def test_upper(self): 6 | self.assertEqual('foo'.upper(), 'FOO') 7 | 8 | def test_isupper(self): 9 | self.assertTrue('FOO'.isupper()) 10 | self.assertFalse('Foo'.isupper()) 11 | 12 | def test_split(self): 13 | s = 'hello world' 14 | self.assertEqual(s.split(), ['hello', 'world']) 15 | # check that s.split fails when the separator is not a string 16 | with self.assertRaises(TypeError): 17 | s.split(2) 18 | 19 | if __name__ == '__main__': 20 | unittest.main() 21 | 22 | def test_upper(): 23 | assert "FOO".lower() == 'foo' 24 | -------------------------------------------------------------------------------- /ch09-documentation/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "restructuredtext.confPath": "${workspaceFolder}/docs/source" 3 | } -------------------------------------------------------------------------------- /ch09-documentation/calculator.py: -------------------------------------------------------------------------------- 1 | from math_operations import add, divide, multiply, subtract 2 | 3 | 4 | class Calculator: 5 | """Simple calculator with basic math operations.""" 6 | 7 | def __init__(self, value=0): 8 | """Initialize the value of the calculator.""" 9 | self.value = value 10 | 11 | def __repr__(self): 12 | """Return the current value.""" 13 | return f"Calculator({self.value})" 14 | 15 | def add(self, operand): 16 | """Add operand to the current value of the calculator. 17 | 18 | >>> from calculator import Calculator 19 | >>> calculator = Calculator(5) 20 | >>> calculator.add(10) 21 | Calculator(15) 22 | """ 23 | self.value = add(self.value, operand) 24 | return self 25 | 26 | def subtract(self, operand): 27 | self.value = subtract(self.value, operand) 28 | return self 29 | 30 | def multiply(self, operand): 31 | self.value = multiply(self.value, operand) 32 | return self 33 | 34 | def divide(self, operand): 35 | self.value = divide(self.value, operand) 36 | return self 37 | -------------------------------------------------------------------------------- /ch09-documentation/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /ch09-documentation/docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: 98ff5aae0d77ee853f64c654ada7118b 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/.doctrees/environment.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/ch09-documentation/docs/source/_build/html/.doctrees/environment.pickle -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/.doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/ch09-documentation/docs/source/_build/html/.doctrees/index.doctree -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/.doctrees/quickstart.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/ch09-documentation/docs/source/_build/html/.doctrees/quickstart.doctree -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/.doctrees/tutorials/tutorial1.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/ch09-documentation/docs/source/_build/html/.doctrees/tutorials/tutorial1.doctree -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/.doctrees/tutorials/tutorial2.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/ch09-documentation/docs/source/_build/html/.doctrees/tutorials/tutorial2.doctree -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/_sources/index.rst.txt: -------------------------------------------------------------------------------- 1 | .. Simple calculator documentation master file, created by 2 | sphinx-quickstart 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Simple calculator's documentation! 7 | ============================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/_sources/quickstart.rst.txt: -------------------------------------------------------------------------------- 1 | Quickstart guide 2 | ================ 3 | 4 | This quickstart guide explains how to use our calculator. 5 | 6 | Examples 7 | -------- 8 | .. code-block:: python 9 | 10 | from calculator import Calculator 11 | calculator = Calculator(5) # Calculator(5) 12 | calculator.add(10) # Calculator(15) 13 | calculator.multiply(10) # Calculator(150) 14 | -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/_sources/tutorials/tutorial1.rst.txt: -------------------------------------------------------------------------------- 1 | Tutorial 1 2 | ========== 3 | 4 | * Step 1 5 | * Step 2 6 | * Step 3 -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/_sources/tutorials/tutorial2.rst.txt: -------------------------------------------------------------------------------- 1 | Tutorial 2 2 | ========== 3 | 4 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/_static/alabaster.css: -------------------------------------------------------------------------------- 1 | @import url("basic.css"); 2 | 3 | /* -- page layout ----------------------------------------------------------- */ 4 | 5 | body { 6 | font-family: Georgia, serif; 7 | font-size: 17px; 8 | background-color: #fff; 9 | color: #000; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | 15 | div.document { 16 | width: 940px; 17 | margin: 30px auto 0 auto; 18 | } 19 | 20 | div.documentwrapper { 21 | float: left; 22 | width: 100%; 23 | } 24 | 25 | div.bodywrapper { 26 | margin: 0 0 0 220px; 27 | } 28 | 29 | div.sphinxsidebar { 30 | width: 220px; 31 | font-size: 14px; 32 | line-height: 1.5; 33 | } 34 | 35 | hr { 36 | border: 1px solid #B1B4B6; 37 | } 38 | 39 | div.body { 40 | background-color: #fff; 41 | color: #3E4349; 42 | padding: 0 30px 0 30px; 43 | } 44 | 45 | div.body > .section { 46 | text-align: left; 47 | } 48 | 49 | div.footer { 50 | width: 940px; 51 | margin: 20px auto 30px auto; 52 | font-size: 14px; 53 | color: #888; 54 | text-align: right; 55 | } 56 | 57 | div.footer a { 58 | color: #888; 59 | } 60 | 61 | p.caption { 62 | font-family: inherit; 63 | font-size: inherit; 64 | } 65 | 66 | 67 | div.relations { 68 | display: none; 69 | } 70 | 71 | 72 | div.sphinxsidebar a { 73 | color: #444; 74 | text-decoration: none; 75 | border-bottom: 1px dotted #999; 76 | } 77 | 78 | div.sphinxsidebar a:hover { 79 | border-bottom: 1px solid #999; 80 | } 81 | 82 | div.sphinxsidebarwrapper { 83 | padding: 18px 10px; 84 | } 85 | 86 | div.sphinxsidebarwrapper p.logo { 87 | padding: 0; 88 | margin: -10px 0 0 0px; 89 | text-align: center; 90 | } 91 | 92 | div.sphinxsidebarwrapper h1.logo { 93 | margin-top: -10px; 94 | text-align: center; 95 | margin-bottom: 5px; 96 | text-align: left; 97 | } 98 | 99 | div.sphinxsidebarwrapper h1.logo-name { 100 | margin-top: 0px; 101 | } 102 | 103 | div.sphinxsidebarwrapper p.blurb { 104 | margin-top: 0; 105 | font-style: normal; 106 | } 107 | 108 | div.sphinxsidebar h3, 109 | div.sphinxsidebar h4 { 110 | font-family: Georgia, serif; 111 | color: #444; 112 | font-size: 24px; 113 | font-weight: normal; 114 | margin: 0 0 5px 0; 115 | padding: 0; 116 | } 117 | 118 | div.sphinxsidebar h4 { 119 | font-size: 20px; 120 | } 121 | 122 | div.sphinxsidebar h3 a { 123 | color: #444; 124 | } 125 | 126 | div.sphinxsidebar p.logo a, 127 | div.sphinxsidebar h3 a, 128 | div.sphinxsidebar p.logo a:hover, 129 | div.sphinxsidebar h3 a:hover { 130 | border: none; 131 | } 132 | 133 | div.sphinxsidebar p { 134 | color: #555; 135 | margin: 10px 0; 136 | } 137 | 138 | div.sphinxsidebar ul { 139 | margin: 10px 0; 140 | padding: 0; 141 | color: #000; 142 | } 143 | 144 | div.sphinxsidebar ul li.toctree-l1 > a { 145 | font-size: 120%; 146 | } 147 | 148 | div.sphinxsidebar ul li.toctree-l2 > a { 149 | font-size: 110%; 150 | } 151 | 152 | div.sphinxsidebar input { 153 | border: 1px solid #CCC; 154 | font-family: Georgia, serif; 155 | font-size: 1em; 156 | } 157 | 158 | div.sphinxsidebar hr { 159 | border: none; 160 | height: 1px; 161 | color: #AAA; 162 | background: #AAA; 163 | 164 | text-align: left; 165 | margin-left: 0; 166 | width: 50%; 167 | } 168 | 169 | div.sphinxsidebar .badge { 170 | border-bottom: none; 171 | } 172 | 173 | div.sphinxsidebar .badge:hover { 174 | border-bottom: none; 175 | } 176 | 177 | /* To address an issue with donation coming after search */ 178 | div.sphinxsidebar h3.donation { 179 | margin-top: 10px; 180 | } 181 | 182 | /* -- body styles ----------------------------------------------------------- */ 183 | 184 | a { 185 | color: #004B6B; 186 | text-decoration: underline; 187 | } 188 | 189 | a:hover { 190 | color: #6D4100; 191 | text-decoration: underline; 192 | } 193 | 194 | div.body h1, 195 | div.body h2, 196 | div.body h3, 197 | div.body h4, 198 | div.body h5, 199 | div.body h6 { 200 | font-family: Georgia, serif; 201 | font-weight: normal; 202 | margin: 30px 0px 10px 0px; 203 | padding: 0; 204 | } 205 | 206 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } 207 | div.body h2 { font-size: 180%; } 208 | div.body h3 { font-size: 150%; } 209 | div.body h4 { font-size: 130%; } 210 | div.body h5 { font-size: 100%; } 211 | div.body h6 { font-size: 100%; } 212 | 213 | a.headerlink { 214 | color: #DDD; 215 | padding: 0 4px; 216 | text-decoration: none; 217 | } 218 | 219 | a.headerlink:hover { 220 | color: #444; 221 | background: #EAEAEA; 222 | } 223 | 224 | div.body p, div.body dd, div.body li { 225 | line-height: 1.4em; 226 | } 227 | 228 | div.admonition { 229 | margin: 20px 0px; 230 | padding: 10px 30px; 231 | background-color: #EEE; 232 | border: 1px solid #CCC; 233 | } 234 | 235 | div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { 236 | background-color: #FBFBFB; 237 | border-bottom: 1px solid #fafafa; 238 | } 239 | 240 | div.admonition p.admonition-title { 241 | font-family: Georgia, serif; 242 | font-weight: normal; 243 | font-size: 24px; 244 | margin: 0 0 10px 0; 245 | padding: 0; 246 | line-height: 1; 247 | } 248 | 249 | div.admonition p.last { 250 | margin-bottom: 0; 251 | } 252 | 253 | div.highlight { 254 | background-color: #fff; 255 | } 256 | 257 | dt:target, .highlight { 258 | background: #FAF3E8; 259 | } 260 | 261 | div.warning { 262 | background-color: #FCC; 263 | border: 1px solid #FAA; 264 | } 265 | 266 | div.danger { 267 | background-color: #FCC; 268 | border: 1px solid #FAA; 269 | -moz-box-shadow: 2px 2px 4px #D52C2C; 270 | -webkit-box-shadow: 2px 2px 4px #D52C2C; 271 | box-shadow: 2px 2px 4px #D52C2C; 272 | } 273 | 274 | div.error { 275 | background-color: #FCC; 276 | border: 1px solid #FAA; 277 | -moz-box-shadow: 2px 2px 4px #D52C2C; 278 | -webkit-box-shadow: 2px 2px 4px #D52C2C; 279 | box-shadow: 2px 2px 4px #D52C2C; 280 | } 281 | 282 | div.caution { 283 | background-color: #FCC; 284 | border: 1px solid #FAA; 285 | } 286 | 287 | div.attention { 288 | background-color: #FCC; 289 | border: 1px solid #FAA; 290 | } 291 | 292 | div.important { 293 | background-color: #EEE; 294 | border: 1px solid #CCC; 295 | } 296 | 297 | div.note { 298 | background-color: #EEE; 299 | border: 1px solid #CCC; 300 | } 301 | 302 | div.tip { 303 | background-color: #EEE; 304 | border: 1px solid #CCC; 305 | } 306 | 307 | div.hint { 308 | background-color: #EEE; 309 | border: 1px solid #CCC; 310 | } 311 | 312 | div.seealso { 313 | background-color: #EEE; 314 | border: 1px solid #CCC; 315 | } 316 | 317 | div.topic { 318 | background-color: #EEE; 319 | } 320 | 321 | p.admonition-title { 322 | display: inline; 323 | } 324 | 325 | p.admonition-title:after { 326 | content: ":"; 327 | } 328 | 329 | pre, tt, code { 330 | font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 331 | font-size: 0.9em; 332 | } 333 | 334 | .hll { 335 | background-color: #FFC; 336 | margin: 0 -12px; 337 | padding: 0 12px; 338 | display: block; 339 | } 340 | 341 | img.screenshot { 342 | } 343 | 344 | tt.descname, tt.descclassname, code.descname, code.descclassname { 345 | font-size: 0.95em; 346 | } 347 | 348 | tt.descname, code.descname { 349 | padding-right: 0.08em; 350 | } 351 | 352 | img.screenshot { 353 | -moz-box-shadow: 2px 2px 4px #EEE; 354 | -webkit-box-shadow: 2px 2px 4px #EEE; 355 | box-shadow: 2px 2px 4px #EEE; 356 | } 357 | 358 | table.docutils { 359 | border: 1px solid #888; 360 | -moz-box-shadow: 2px 2px 4px #EEE; 361 | -webkit-box-shadow: 2px 2px 4px #EEE; 362 | box-shadow: 2px 2px 4px #EEE; 363 | } 364 | 365 | table.docutils td, table.docutils th { 366 | border: 1px solid #888; 367 | padding: 0.25em 0.7em; 368 | } 369 | 370 | table.field-list, table.footnote { 371 | border: none; 372 | -moz-box-shadow: none; 373 | -webkit-box-shadow: none; 374 | box-shadow: none; 375 | } 376 | 377 | table.footnote { 378 | margin: 15px 0; 379 | width: 100%; 380 | border: 1px solid #EEE; 381 | background: #FDFDFD; 382 | font-size: 0.9em; 383 | } 384 | 385 | table.footnote + table.footnote { 386 | margin-top: -15px; 387 | border-top: none; 388 | } 389 | 390 | table.field-list th { 391 | padding: 0 0.8em 0 0; 392 | } 393 | 394 | table.field-list td { 395 | padding: 0; 396 | } 397 | 398 | table.field-list p { 399 | margin-bottom: 0.8em; 400 | } 401 | 402 | /* Cloned from 403 | * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 404 | */ 405 | .field-name { 406 | -moz-hyphens: manual; 407 | -ms-hyphens: manual; 408 | -webkit-hyphens: manual; 409 | hyphens: manual; 410 | } 411 | 412 | table.footnote td.label { 413 | width: .1px; 414 | padding: 0.3em 0 0.3em 0.5em; 415 | } 416 | 417 | table.footnote td { 418 | padding: 0.3em 0.5em; 419 | } 420 | 421 | dl { 422 | margin: 0; 423 | padding: 0; 424 | } 425 | 426 | dl dd { 427 | margin-left: 30px; 428 | } 429 | 430 | blockquote { 431 | margin: 0 0 0 30px; 432 | padding: 0; 433 | } 434 | 435 | ul, ol { 436 | /* Matches the 30px from the narrow-screen "li > ul" selector below */ 437 | margin: 10px 0 10px 30px; 438 | padding: 0; 439 | } 440 | 441 | pre { 442 | background: #EEE; 443 | padding: 7px 30px; 444 | margin: 15px 0px; 445 | line-height: 1.3em; 446 | } 447 | 448 | div.viewcode-block:target { 449 | background: #ffd; 450 | } 451 | 452 | dl pre, blockquote pre, li pre { 453 | margin-left: 0; 454 | padding-left: 30px; 455 | } 456 | 457 | tt, code { 458 | background-color: #ecf0f3; 459 | color: #222; 460 | /* padding: 1px 2px; */ 461 | } 462 | 463 | tt.xref, code.xref, a tt { 464 | background-color: #FBFBFB; 465 | border-bottom: 1px solid #fff; 466 | } 467 | 468 | a.reference { 469 | text-decoration: none; 470 | border-bottom: 1px dotted #004B6B; 471 | } 472 | 473 | /* Don't put an underline on images */ 474 | a.image-reference, a.image-reference:hover { 475 | border-bottom: none; 476 | } 477 | 478 | a.reference:hover { 479 | border-bottom: 1px solid #6D4100; 480 | } 481 | 482 | a.footnote-reference { 483 | text-decoration: none; 484 | font-size: 0.7em; 485 | vertical-align: top; 486 | border-bottom: 1px dotted #004B6B; 487 | } 488 | 489 | a.footnote-reference:hover { 490 | border-bottom: 1px solid #6D4100; 491 | } 492 | 493 | a:hover tt, a:hover code { 494 | background: #EEE; 495 | } 496 | 497 | 498 | @media screen and (max-width: 870px) { 499 | 500 | div.sphinxsidebar { 501 | display: none; 502 | } 503 | 504 | div.document { 505 | width: 100%; 506 | 507 | } 508 | 509 | div.documentwrapper { 510 | margin-left: 0; 511 | margin-top: 0; 512 | margin-right: 0; 513 | margin-bottom: 0; 514 | } 515 | 516 | div.bodywrapper { 517 | margin-top: 0; 518 | margin-right: 0; 519 | margin-bottom: 0; 520 | margin-left: 0; 521 | } 522 | 523 | ul { 524 | margin-left: 0; 525 | } 526 | 527 | li > ul { 528 | /* Matches the 30px from the "ul, ol" selector above */ 529 | margin-left: 30px; 530 | } 531 | 532 | .document { 533 | width: auto; 534 | } 535 | 536 | .footer { 537 | width: auto; 538 | } 539 | 540 | .bodywrapper { 541 | margin: 0; 542 | } 543 | 544 | .footer { 545 | width: auto; 546 | } 547 | 548 | .github { 549 | display: none; 550 | } 551 | 552 | 553 | 554 | } 555 | 556 | 557 | 558 | @media screen and (max-width: 875px) { 559 | 560 | body { 561 | margin: 0; 562 | padding: 20px 30px; 563 | } 564 | 565 | div.documentwrapper { 566 | float: none; 567 | background: #fff; 568 | } 569 | 570 | div.sphinxsidebar { 571 | display: block; 572 | float: none; 573 | width: 102.5%; 574 | margin: 50px -30px -20px -30px; 575 | padding: 10px 20px; 576 | background: #333; 577 | color: #FFF; 578 | } 579 | 580 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, 581 | div.sphinxsidebar h3 a { 582 | color: #fff; 583 | } 584 | 585 | div.sphinxsidebar a { 586 | color: #AAA; 587 | } 588 | 589 | div.sphinxsidebar p.logo { 590 | display: none; 591 | } 592 | 593 | div.document { 594 | width: 100%; 595 | margin: 0; 596 | } 597 | 598 | div.footer { 599 | display: none; 600 | } 601 | 602 | div.bodywrapper { 603 | margin: 0; 604 | } 605 | 606 | div.body { 607 | min-height: 0; 608 | padding: 0; 609 | } 610 | 611 | .rtd_doc_footer { 612 | display: none; 613 | } 614 | 615 | .document { 616 | width: auto; 617 | } 618 | 619 | .footer { 620 | width: auto; 621 | } 622 | 623 | .footer { 624 | width: auto; 625 | } 626 | 627 | .github { 628 | display: none; 629 | } 630 | } 631 | 632 | 633 | /* misc. */ 634 | 635 | .revsys-inline { 636 | display: none!important; 637 | } 638 | 639 | /* Make nested-list/multi-paragraph items look better in Releases changelog 640 | * pages. Without this, docutils' magical list fuckery causes inconsistent 641 | * formatting between different release sub-lists. 642 | */ 643 | div#changelog > div.section > ul > li > p:only-child { 644 | margin-bottom: 0; 645 | } 646 | 647 | /* Hide fugly table cell borders in ..bibliography:: directive output */ 648 | table.docutils.citation, table.docutils.citation td, table.docutils.citation th { 649 | border: none; 650 | /* Below needed in some edge cases; if not applied, bottom shadows appear */ 651 | -moz-box-shadow: none; 652 | -webkit-box-shadow: none; 653 | box-shadow: none; 654 | } 655 | 656 | 657 | /* relbar */ 658 | 659 | .related { 660 | line-height: 30px; 661 | width: 100%; 662 | font-size: 0.9rem; 663 | } 664 | 665 | .related.top { 666 | border-bottom: 1px solid #EEE; 667 | margin-bottom: 20px; 668 | } 669 | 670 | .related.bottom { 671 | border-top: 1px solid #EEE; 672 | } 673 | 674 | .related ul { 675 | padding: 0; 676 | margin: 0; 677 | list-style: none; 678 | } 679 | 680 | .related li { 681 | display: inline; 682 | } 683 | 684 | nav#rellinks { 685 | float: right; 686 | } 687 | 688 | nav#rellinks li+li:before { 689 | content: "|"; 690 | } 691 | 692 | nav#breadcrumbs li+li:before { 693 | content: "\00BB"; 694 | } 695 | 696 | /* Hide certain items when printing */ 697 | @media print { 698 | div.related { 699 | display: none; 700 | } 701 | } -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* This file intentionally left blank. */ 2 | -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/_static/doctools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * doctools.js 3 | * ~~~~~~~~~~~ 4 | * 5 | * Sphinx JavaScript utilities for all documentation. 6 | * 7 | * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /** 13 | * select a different prefix for underscore 14 | */ 15 | $u = _.noConflict(); 16 | 17 | /** 18 | * make the code below compatible with browsers without 19 | * an installed firebug like debugger 20 | if (!window.console || !console.firebug) { 21 | var names = ["log", "debug", "info", "warn", "error", "assert", "dir", 22 | "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", 23 | "profile", "profileEnd"]; 24 | window.console = {}; 25 | for (var i = 0; i < names.length; ++i) 26 | window.console[names[i]] = function() {}; 27 | } 28 | */ 29 | 30 | /** 31 | * small helper function to urldecode strings 32 | */ 33 | jQuery.urldecode = function(x) { 34 | return decodeURIComponent(x).replace(/\+/g, ' '); 35 | }; 36 | 37 | /** 38 | * small helper function to urlencode strings 39 | */ 40 | jQuery.urlencode = encodeURIComponent; 41 | 42 | /** 43 | * This function returns the parsed url parameters of the 44 | * current request. Multiple values per key are supported, 45 | * it will always return arrays of strings for the value parts. 46 | */ 47 | jQuery.getQueryParameters = function(s) { 48 | if (typeof s === 'undefined') 49 | s = document.location.search; 50 | var parts = s.substr(s.indexOf('?') + 1).split('&'); 51 | var result = {}; 52 | for (var i = 0; i < parts.length; i++) { 53 | var tmp = parts[i].split('=', 2); 54 | var key = jQuery.urldecode(tmp[0]); 55 | var value = jQuery.urldecode(tmp[1]); 56 | if (key in result) 57 | result[key].push(value); 58 | else 59 | result[key] = [value]; 60 | } 61 | return result; 62 | }; 63 | 64 | /** 65 | * highlight a given string on a jquery object by wrapping it in 66 | * span elements with the given class name. 67 | */ 68 | jQuery.fn.highlightText = function(text, className) { 69 | function highlight(node, addItems) { 70 | if (node.nodeType === 3) { 71 | var val = node.nodeValue; 72 | var pos = val.toLowerCase().indexOf(text); 73 | if (pos >= 0 && 74 | !jQuery(node.parentNode).hasClass(className) && 75 | !jQuery(node.parentNode).hasClass("nohighlight")) { 76 | var span; 77 | var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); 78 | if (isInSVG) { 79 | span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); 80 | } else { 81 | span = document.createElement("span"); 82 | span.className = className; 83 | } 84 | span.appendChild(document.createTextNode(val.substr(pos, text.length))); 85 | node.parentNode.insertBefore(span, node.parentNode.insertBefore( 86 | document.createTextNode(val.substr(pos + text.length)), 87 | node.nextSibling)); 88 | node.nodeValue = val.substr(0, pos); 89 | if (isInSVG) { 90 | var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); 91 | var bbox = node.parentElement.getBBox(); 92 | rect.x.baseVal.value = bbox.x; 93 | rect.y.baseVal.value = bbox.y; 94 | rect.width.baseVal.value = bbox.width; 95 | rect.height.baseVal.value = bbox.height; 96 | rect.setAttribute('class', className); 97 | addItems.push({ 98 | "parent": node.parentNode, 99 | "target": rect}); 100 | } 101 | } 102 | } 103 | else if (!jQuery(node).is("button, select, textarea")) { 104 | jQuery.each(node.childNodes, function() { 105 | highlight(this, addItems); 106 | }); 107 | } 108 | } 109 | var addItems = []; 110 | var result = this.each(function() { 111 | highlight(this, addItems); 112 | }); 113 | for (var i = 0; i < addItems.length; ++i) { 114 | jQuery(addItems[i].parent).before(addItems[i].target); 115 | } 116 | return result; 117 | }; 118 | 119 | /* 120 | * backward compatibility for jQuery.browser 121 | * This will be supported until firefox bug is fixed. 122 | */ 123 | if (!jQuery.browser) { 124 | jQuery.uaMatch = function(ua) { 125 | ua = ua.toLowerCase(); 126 | 127 | var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || 128 | /(webkit)[ \/]([\w.]+)/.exec(ua) || 129 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || 130 | /(msie) ([\w.]+)/.exec(ua) || 131 | ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || 132 | []; 133 | 134 | return { 135 | browser: match[ 1 ] || "", 136 | version: match[ 2 ] || "0" 137 | }; 138 | }; 139 | jQuery.browser = {}; 140 | jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; 141 | } 142 | 143 | /** 144 | * Small JavaScript module for the documentation. 145 | */ 146 | var Documentation = { 147 | 148 | init : function() { 149 | this.fixFirefoxAnchorBug(); 150 | this.highlightSearchWords(); 151 | this.initIndexTable(); 152 | if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) { 153 | this.initOnKeyListeners(); 154 | } 155 | }, 156 | 157 | /** 158 | * i18n support 159 | */ 160 | TRANSLATIONS : {}, 161 | PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; }, 162 | LOCALE : 'unknown', 163 | 164 | // gettext and ngettext don't access this so that the functions 165 | // can safely bound to a different name (_ = Documentation.gettext) 166 | gettext : function(string) { 167 | var translated = Documentation.TRANSLATIONS[string]; 168 | if (typeof translated === 'undefined') 169 | return string; 170 | return (typeof translated === 'string') ? translated : translated[0]; 171 | }, 172 | 173 | ngettext : function(singular, plural, n) { 174 | var translated = Documentation.TRANSLATIONS[singular]; 175 | if (typeof translated === 'undefined') 176 | return (n == 1) ? singular : plural; 177 | return translated[Documentation.PLURALEXPR(n)]; 178 | }, 179 | 180 | addTranslations : function(catalog) { 181 | for (var key in catalog.messages) 182 | this.TRANSLATIONS[key] = catalog.messages[key]; 183 | this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); 184 | this.LOCALE = catalog.locale; 185 | }, 186 | 187 | /** 188 | * add context elements like header anchor links 189 | */ 190 | addContextElements : function() { 191 | $('div[id] > :header:first').each(function() { 192 | $('\u00B6'). 193 | attr('href', '#' + this.id). 194 | attr('title', _('Permalink to this headline')). 195 | appendTo(this); 196 | }); 197 | $('dt[id]').each(function() { 198 | $('\u00B6'). 199 | attr('href', '#' + this.id). 200 | attr('title', _('Permalink to this definition')). 201 | appendTo(this); 202 | }); 203 | }, 204 | 205 | /** 206 | * workaround a firefox stupidity 207 | * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 208 | */ 209 | fixFirefoxAnchorBug : function() { 210 | if (document.location.hash && $.browser.mozilla) 211 | window.setTimeout(function() { 212 | document.location.href += ''; 213 | }, 10); 214 | }, 215 | 216 | /** 217 | * highlight the search words provided in the url in the text 218 | */ 219 | highlightSearchWords : function() { 220 | var params = $.getQueryParameters(); 221 | var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; 222 | if (terms.length) { 223 | var body = $('div.body'); 224 | if (!body.length) { 225 | body = $('body'); 226 | } 227 | window.setTimeout(function() { 228 | $.each(terms, function() { 229 | body.highlightText(this.toLowerCase(), 'highlighted'); 230 | }); 231 | }, 10); 232 | $('') 234 | .appendTo($('#searchbox')); 235 | } 236 | }, 237 | 238 | /** 239 | * init the domain index toggle buttons 240 | */ 241 | initIndexTable : function() { 242 | var togglers = $('img.toggler').click(function() { 243 | var src = $(this).attr('src'); 244 | var idnum = $(this).attr('id').substr(7); 245 | $('tr.cg-' + idnum).toggle(); 246 | if (src.substr(-9) === 'minus.png') 247 | $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); 248 | else 249 | $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); 250 | }).css('display', ''); 251 | if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { 252 | togglers.click(); 253 | } 254 | }, 255 | 256 | /** 257 | * helper function to hide the search marks again 258 | */ 259 | hideSearchWords : function() { 260 | $('#searchbox .highlight-link').fadeOut(300); 261 | $('span.highlighted').removeClass('highlighted'); 262 | }, 263 | 264 | /** 265 | * make the url absolute 266 | */ 267 | makeURL : function(relativeURL) { 268 | return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; 269 | }, 270 | 271 | /** 272 | * get the current relative url 273 | */ 274 | getCurrentURL : function() { 275 | var path = document.location.pathname; 276 | var parts = path.split(/\//); 277 | $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { 278 | if (this === '..') 279 | parts.pop(); 280 | }); 281 | var url = parts.join('/'); 282 | return path.substring(url.lastIndexOf('/') + 1, path.length - 1); 283 | }, 284 | 285 | initOnKeyListeners: function() { 286 | $(document).keydown(function(event) { 287 | var activeElementType = document.activeElement.tagName; 288 | // don't navigate when in search box, textarea, dropdown or button 289 | if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT' 290 | && activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey 291 | && !event.shiftKey) { 292 | switch (event.keyCode) { 293 | case 37: // left 294 | var prevHref = $('link[rel="prev"]').prop('href'); 295 | if (prevHref) { 296 | window.location.href = prevHref; 297 | return false; 298 | } 299 | case 39: // right 300 | var nextHref = $('link[rel="next"]').prop('href'); 301 | if (nextHref) { 302 | window.location.href = nextHref; 303 | return false; 304 | } 305 | } 306 | } 307 | }); 308 | } 309 | }; 310 | 311 | // quick alias for translations 312 | _ = Documentation.gettext; 313 | 314 | $(document).ready(function() { 315 | Documentation.init(); 316 | }); 317 | -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/_static/documentation_options.js: -------------------------------------------------------------------------------- 1 | var DOCUMENTATION_OPTIONS = { 2 | URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), 3 | VERSION: '1.0', 4 | LANGUAGE: 'None', 5 | COLLAPSE_INDEX: false, 6 | BUILDER: 'html', 7 | FILE_SUFFIX: '.html', 8 | LINK_SUFFIX: '.html', 9 | HAS_SOURCE: true, 10 | SOURCELINK_SUFFIX: '.txt', 11 | NAVIGATION_WITH_KEYS: false 12 | }; -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/ch09-documentation/docs/source/_build/html/_static/file.png -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/_static/language_data.js: -------------------------------------------------------------------------------- 1 | /* 2 | * language_data.js 3 | * ~~~~~~~~~~~~~~~~ 4 | * 5 | * This script contains the language-specific data used by searchtools.js, 6 | * namely the list of stopwords, stemmer, scorer and splitter. 7 | * 8 | * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. 9 | * :license: BSD, see LICENSE for details. 10 | * 11 | */ 12 | 13 | var stopwords = ["a","and","are","as","at","be","but","by","for","if","in","into","is","it","near","no","not","of","on","or","such","that","the","their","then","there","these","they","this","to","was","will","with"]; 14 | 15 | 16 | /* Non-minified version JS is _stemmer.js if file is provided */ 17 | /** 18 | * Porter Stemmer 19 | */ 20 | var Stemmer = function() { 21 | 22 | var step2list = { 23 | ational: 'ate', 24 | tional: 'tion', 25 | enci: 'ence', 26 | anci: 'ance', 27 | izer: 'ize', 28 | bli: 'ble', 29 | alli: 'al', 30 | entli: 'ent', 31 | eli: 'e', 32 | ousli: 'ous', 33 | ization: 'ize', 34 | ation: 'ate', 35 | ator: 'ate', 36 | alism: 'al', 37 | iveness: 'ive', 38 | fulness: 'ful', 39 | ousness: 'ous', 40 | aliti: 'al', 41 | iviti: 'ive', 42 | biliti: 'ble', 43 | logi: 'log' 44 | }; 45 | 46 | var step3list = { 47 | icate: 'ic', 48 | ative: '', 49 | alize: 'al', 50 | iciti: 'ic', 51 | ical: 'ic', 52 | ful: '', 53 | ness: '' 54 | }; 55 | 56 | var c = "[^aeiou]"; // consonant 57 | var v = "[aeiouy]"; // vowel 58 | var C = c + "[^aeiouy]*"; // consonant sequence 59 | var V = v + "[aeiou]*"; // vowel sequence 60 | 61 | var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 62 | var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 63 | var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 64 | var s_v = "^(" + C + ")?" + v; // vowel in stem 65 | 66 | this.stemWord = function (w) { 67 | var stem; 68 | var suffix; 69 | var firstch; 70 | var origword = w; 71 | 72 | if (w.length < 3) 73 | return w; 74 | 75 | var re; 76 | var re2; 77 | var re3; 78 | var re4; 79 | 80 | firstch = w.substr(0,1); 81 | if (firstch == "y") 82 | w = firstch.toUpperCase() + w.substr(1); 83 | 84 | // Step 1a 85 | re = /^(.+?)(ss|i)es$/; 86 | re2 = /^(.+?)([^s])s$/; 87 | 88 | if (re.test(w)) 89 | w = w.replace(re,"$1$2"); 90 | else if (re2.test(w)) 91 | w = w.replace(re2,"$1$2"); 92 | 93 | // Step 1b 94 | re = /^(.+?)eed$/; 95 | re2 = /^(.+?)(ed|ing)$/; 96 | if (re.test(w)) { 97 | var fp = re.exec(w); 98 | re = new RegExp(mgr0); 99 | if (re.test(fp[1])) { 100 | re = /.$/; 101 | w = w.replace(re,""); 102 | } 103 | } 104 | else if (re2.test(w)) { 105 | var fp = re2.exec(w); 106 | stem = fp[1]; 107 | re2 = new RegExp(s_v); 108 | if (re2.test(stem)) { 109 | w = stem; 110 | re2 = /(at|bl|iz)$/; 111 | re3 = new RegExp("([^aeiouylsz])\\1$"); 112 | re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); 113 | if (re2.test(w)) 114 | w = w + "e"; 115 | else if (re3.test(w)) { 116 | re = /.$/; 117 | w = w.replace(re,""); 118 | } 119 | else if (re4.test(w)) 120 | w = w + "e"; 121 | } 122 | } 123 | 124 | // Step 1c 125 | re = /^(.+?)y$/; 126 | if (re.test(w)) { 127 | var fp = re.exec(w); 128 | stem = fp[1]; 129 | re = new RegExp(s_v); 130 | if (re.test(stem)) 131 | w = stem + "i"; 132 | } 133 | 134 | // Step 2 135 | re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; 136 | if (re.test(w)) { 137 | var fp = re.exec(w); 138 | stem = fp[1]; 139 | suffix = fp[2]; 140 | re = new RegExp(mgr0); 141 | if (re.test(stem)) 142 | w = stem + step2list[suffix]; 143 | } 144 | 145 | // Step 3 146 | re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; 147 | if (re.test(w)) { 148 | var fp = re.exec(w); 149 | stem = fp[1]; 150 | suffix = fp[2]; 151 | re = new RegExp(mgr0); 152 | if (re.test(stem)) 153 | w = stem + step3list[suffix]; 154 | } 155 | 156 | // Step 4 157 | re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; 158 | re2 = /^(.+?)(s|t)(ion)$/; 159 | if (re.test(w)) { 160 | var fp = re.exec(w); 161 | stem = fp[1]; 162 | re = new RegExp(mgr1); 163 | if (re.test(stem)) 164 | w = stem; 165 | } 166 | else if (re2.test(w)) { 167 | var fp = re2.exec(w); 168 | stem = fp[1] + fp[2]; 169 | re2 = new RegExp(mgr1); 170 | if (re2.test(stem)) 171 | w = stem; 172 | } 173 | 174 | // Step 5 175 | re = /^(.+?)e$/; 176 | if (re.test(w)) { 177 | var fp = re.exec(w); 178 | stem = fp[1]; 179 | re = new RegExp(mgr1); 180 | re2 = new RegExp(meq1); 181 | re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); 182 | if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) 183 | w = stem; 184 | } 185 | re = /ll$/; 186 | re2 = new RegExp(mgr1); 187 | if (re.test(w) && re2.test(w)) { 188 | re = /.$/; 189 | w = w.replace(re,""); 190 | } 191 | 192 | // and turn initial Y back to y 193 | if (firstch == "y") 194 | w = firstch.toLowerCase() + w.substr(1); 195 | return w; 196 | } 197 | } 198 | 199 | 200 | 201 | 202 | 203 | var splitChars = (function() { 204 | var result = {}; 205 | var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648, 206 | 1748, 1809, 2416, 2473, 2481, 2526, 2601, 2609, 2612, 2615, 2653, 2702, 207 | 2706, 2729, 2737, 2740, 2857, 2865, 2868, 2910, 2928, 2948, 2961, 2971, 208 | 2973, 3085, 3089, 3113, 3124, 3213, 3217, 3241, 3252, 3295, 3341, 3345, 209 | 3369, 3506, 3516, 3633, 3715, 3721, 3736, 3744, 3748, 3750, 3756, 3761, 210 | 3781, 3912, 4239, 4347, 4681, 4695, 4697, 4745, 4785, 4799, 4801, 4823, 211 | 4881, 5760, 5901, 5997, 6313, 7405, 8024, 8026, 8028, 8030, 8117, 8125, 212 | 8133, 8181, 8468, 8485, 8487, 8489, 8494, 8527, 11311, 11359, 11687, 11695, 213 | 11703, 11711, 11719, 11727, 11735, 12448, 12539, 43010, 43014, 43019, 43587, 214 | 43696, 43713, 64286, 64297, 64311, 64317, 64319, 64322, 64325, 65141]; 215 | var i, j, start, end; 216 | for (i = 0; i < singles.length; i++) { 217 | result[singles[i]] = true; 218 | } 219 | var ranges = [[0, 47], [58, 64], [91, 94], [123, 169], [171, 177], [182, 184], [706, 709], 220 | [722, 735], [741, 747], [751, 879], [888, 889], [894, 901], [1154, 1161], 221 | [1318, 1328], [1367, 1368], [1370, 1376], [1416, 1487], [1515, 1519], [1523, 1568], 222 | [1611, 1631], [1642, 1645], [1750, 1764], [1767, 1773], [1789, 1790], [1792, 1807], 223 | [1840, 1868], [1958, 1968], [1970, 1983], [2027, 2035], [2038, 2041], [2043, 2047], 224 | [2070, 2073], [2075, 2083], [2085, 2087], [2089, 2307], [2362, 2364], [2366, 2383], 225 | [2385, 2391], [2402, 2405], [2419, 2424], [2432, 2436], [2445, 2446], [2449, 2450], 226 | [2483, 2485], [2490, 2492], [2494, 2509], [2511, 2523], [2530, 2533], [2546, 2547], 227 | [2554, 2564], [2571, 2574], [2577, 2578], [2618, 2648], [2655, 2661], [2672, 2673], 228 | [2677, 2692], [2746, 2748], [2750, 2767], [2769, 2783], [2786, 2789], [2800, 2820], 229 | [2829, 2830], [2833, 2834], [2874, 2876], [2878, 2907], [2914, 2917], [2930, 2946], 230 | [2955, 2957], [2966, 2968], [2976, 2978], [2981, 2983], [2987, 2989], [3002, 3023], 231 | [3025, 3045], [3059, 3076], [3130, 3132], [3134, 3159], [3162, 3167], [3170, 3173], 232 | [3184, 3191], [3199, 3204], [3258, 3260], [3262, 3293], [3298, 3301], [3312, 3332], 233 | [3386, 3388], [3390, 3423], [3426, 3429], [3446, 3449], [3456, 3460], [3479, 3481], 234 | [3518, 3519], [3527, 3584], [3636, 3647], [3655, 3663], [3674, 3712], [3717, 3718], 235 | [3723, 3724], [3726, 3731], [3752, 3753], [3764, 3772], [3774, 3775], [3783, 3791], 236 | [3802, 3803], [3806, 3839], [3841, 3871], [3892, 3903], [3949, 3975], [3980, 4095], 237 | [4139, 4158], [4170, 4175], [4182, 4185], [4190, 4192], [4194, 4196], [4199, 4205], 238 | [4209, 4212], [4226, 4237], [4250, 4255], [4294, 4303], [4349, 4351], [4686, 4687], 239 | [4702, 4703], [4750, 4751], [4790, 4791], [4806, 4807], [4886, 4887], [4955, 4968], 240 | [4989, 4991], [5008, 5023], [5109, 5120], [5741, 5742], [5787, 5791], [5867, 5869], 241 | [5873, 5887], [5906, 5919], [5938, 5951], [5970, 5983], [6001, 6015], [6068, 6102], 242 | [6104, 6107], [6109, 6111], [6122, 6127], [6138, 6159], [6170, 6175], [6264, 6271], 243 | [6315, 6319], [6390, 6399], [6429, 6469], [6510, 6511], [6517, 6527], [6572, 6592], 244 | [6600, 6607], [6619, 6655], [6679, 6687], [6741, 6783], [6794, 6799], [6810, 6822], 245 | [6824, 6916], [6964, 6980], [6988, 6991], [7002, 7042], [7073, 7085], [7098, 7167], 246 | [7204, 7231], [7242, 7244], [7294, 7400], [7410, 7423], [7616, 7679], [7958, 7959], 247 | [7966, 7967], [8006, 8007], [8014, 8015], [8062, 8063], [8127, 8129], [8141, 8143], 248 | [8148, 8149], [8156, 8159], [8173, 8177], [8189, 8303], [8306, 8307], [8314, 8318], 249 | [8330, 8335], [8341, 8449], [8451, 8454], [8456, 8457], [8470, 8472], [8478, 8483], 250 | [8506, 8507], [8512, 8516], [8522, 8525], [8586, 9311], [9372, 9449], [9472, 10101], 251 | [10132, 11263], [11493, 11498], [11503, 11516], [11518, 11519], [11558, 11567], 252 | [11622, 11630], [11632, 11647], [11671, 11679], [11743, 11822], [11824, 12292], 253 | [12296, 12320], [12330, 12336], [12342, 12343], [12349, 12352], [12439, 12444], 254 | [12544, 12548], [12590, 12592], [12687, 12689], [12694, 12703], [12728, 12783], 255 | [12800, 12831], [12842, 12880], [12896, 12927], [12938, 12976], [12992, 13311], 256 | [19894, 19967], [40908, 40959], [42125, 42191], [42238, 42239], [42509, 42511], 257 | [42540, 42559], [42592, 42593], [42607, 42622], [42648, 42655], [42736, 42774], 258 | [42784, 42785], [42889, 42890], [42893, 43002], [43043, 43055], [43062, 43071], 259 | [43124, 43137], [43188, 43215], [43226, 43249], [43256, 43258], [43260, 43263], 260 | [43302, 43311], [43335, 43359], [43389, 43395], [43443, 43470], [43482, 43519], 261 | [43561, 43583], [43596, 43599], [43610, 43615], [43639, 43641], [43643, 43647], 262 | [43698, 43700], [43703, 43704], [43710, 43711], [43715, 43738], [43742, 43967], 263 | [44003, 44015], [44026, 44031], [55204, 55215], [55239, 55242], [55292, 55295], 264 | [57344, 63743], [64046, 64047], [64110, 64111], [64218, 64255], [64263, 64274], 265 | [64280, 64284], [64434, 64466], [64830, 64847], [64912, 64913], [64968, 65007], 266 | [65020, 65135], [65277, 65295], [65306, 65312], [65339, 65344], [65371, 65381], 267 | [65471, 65473], [65480, 65481], [65488, 65489], [65496, 65497]]; 268 | for (i = 0; i < ranges.length; i++) { 269 | start = ranges[i][0]; 270 | end = ranges[i][1]; 271 | for (j = start; j <= end; j++) { 272 | result[j] = true; 273 | } 274 | } 275 | return result; 276 | })(); 277 | 278 | function splitQuery(query) { 279 | var result = []; 280 | var start = -1; 281 | for (var i = 0; i < query.length; i++) { 282 | if (splitChars[query.charCodeAt(i)]) { 283 | if (start !== -1) { 284 | result.push(query.slice(start, i)); 285 | start = -1; 286 | } 287 | } else if (start === -1) { 288 | start = i; 289 | } 290 | } 291 | if (start !== -1) { 292 | result.push(query.slice(start)); 293 | } 294 | return result; 295 | } 296 | 297 | 298 | -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/ch09-documentation/docs/source/_build/html/_static/minus.png -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/ch09-documentation/docs/source/_build/html/_static/plus.png -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/_static/pygments.css: -------------------------------------------------------------------------------- 1 | pre { line-height: 125%; margin: 0; } 2 | td.linenos pre { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; } 3 | span.linenos { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; } 4 | td.linenos pre.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 5 | span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 6 | .highlight .hll { background-color: #ffffcc } 7 | .highlight { background: #f8f8f8; } 8 | .highlight .c { color: #8f5902; font-style: italic } /* Comment */ 9 | .highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ 10 | .highlight .g { color: #000000 } /* Generic */ 11 | .highlight .k { color: #004461; font-weight: bold } /* Keyword */ 12 | .highlight .l { color: #000000 } /* Literal */ 13 | .highlight .n { color: #000000 } /* Name */ 14 | .highlight .o { color: #582800 } /* Operator */ 15 | .highlight .x { color: #000000 } /* Other */ 16 | .highlight .p { color: #000000; font-weight: bold } /* Punctuation */ 17 | .highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */ 18 | .highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ 19 | .highlight .cp { color: #8f5902 } /* Comment.Preproc */ 20 | .highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */ 21 | .highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ 22 | .highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */ 23 | .highlight .gd { color: #a40000 } /* Generic.Deleted */ 24 | .highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ 25 | .highlight .gr { color: #ef2929 } /* Generic.Error */ 26 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 27 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 28 | .highlight .go { color: #888888 } /* Generic.Output */ 29 | .highlight .gp { color: #745334 } /* Generic.Prompt */ 30 | .highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */ 31 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 32 | .highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ 33 | .highlight .kc { color: #004461; font-weight: bold } /* Keyword.Constant */ 34 | .highlight .kd { color: #004461; font-weight: bold } /* Keyword.Declaration */ 35 | .highlight .kn { color: #004461; font-weight: bold } /* Keyword.Namespace */ 36 | .highlight .kp { color: #004461; font-weight: bold } /* Keyword.Pseudo */ 37 | .highlight .kr { color: #004461; font-weight: bold } /* Keyword.Reserved */ 38 | .highlight .kt { color: #004461; font-weight: bold } /* Keyword.Type */ 39 | .highlight .ld { color: #000000 } /* Literal.Date */ 40 | .highlight .m { color: #990000 } /* Literal.Number */ 41 | .highlight .s { color: #4e9a06 } /* Literal.String */ 42 | .highlight .na { color: #c4a000 } /* Name.Attribute */ 43 | .highlight .nb { color: #004461 } /* Name.Builtin */ 44 | .highlight .nc { color: #000000 } /* Name.Class */ 45 | .highlight .no { color: #000000 } /* Name.Constant */ 46 | .highlight .nd { color: #888888 } /* Name.Decorator */ 47 | .highlight .ni { color: #ce5c00 } /* Name.Entity */ 48 | .highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ 49 | .highlight .nf { color: #000000 } /* Name.Function */ 50 | .highlight .nl { color: #f57900 } /* Name.Label */ 51 | .highlight .nn { color: #000000 } /* Name.Namespace */ 52 | .highlight .nx { color: #000000 } /* Name.Other */ 53 | .highlight .py { color: #000000 } /* Name.Property */ 54 | .highlight .nt { color: #004461; font-weight: bold } /* Name.Tag */ 55 | .highlight .nv { color: #000000 } /* Name.Variable */ 56 | .highlight .ow { color: #004461; font-weight: bold } /* Operator.Word */ 57 | .highlight .w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */ 58 | .highlight .mb { color: #990000 } /* Literal.Number.Bin */ 59 | .highlight .mf { color: #990000 } /* Literal.Number.Float */ 60 | .highlight .mh { color: #990000 } /* Literal.Number.Hex */ 61 | .highlight .mi { color: #990000 } /* Literal.Number.Integer */ 62 | .highlight .mo { color: #990000 } /* Literal.Number.Oct */ 63 | .highlight .sa { color: #4e9a06 } /* Literal.String.Affix */ 64 | .highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */ 65 | .highlight .sc { color: #4e9a06 } /* Literal.String.Char */ 66 | .highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */ 67 | .highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ 68 | .highlight .s2 { color: #4e9a06 } /* Literal.String.Double */ 69 | .highlight .se { color: #4e9a06 } /* Literal.String.Escape */ 70 | .highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */ 71 | .highlight .si { color: #4e9a06 } /* Literal.String.Interpol */ 72 | .highlight .sx { color: #4e9a06 } /* Literal.String.Other */ 73 | .highlight .sr { color: #4e9a06 } /* Literal.String.Regex */ 74 | .highlight .s1 { color: #4e9a06 } /* Literal.String.Single */ 75 | .highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */ 76 | .highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ 77 | .highlight .fm { color: #000000 } /* Name.Function.Magic */ 78 | .highlight .vc { color: #000000 } /* Name.Variable.Class */ 79 | .highlight .vg { color: #000000 } /* Name.Variable.Global */ 80 | .highlight .vi { color: #000000 } /* Name.Variable.Instance */ 81 | .highlight .vm { color: #000000 } /* Name.Variable.Magic */ 82 | .highlight .il { color: #990000 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/_static/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.3.1 2 | // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Underscore is freely distributable under the MIT license. 4 | // Portions of Underscore are inspired or borrowed from Prototype, 5 | // Oliver Steele's Functional, and John Resig's Micro-Templating. 6 | // For all details and documentation: 7 | // http://documentcloud.github.com/underscore 8 | (function(){function q(a,c,d){if(a===c)return a!==0||1/a==1/c;if(a==null||c==null)return a===c;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return false;switch(e){case "[object String]":return a==String(c);case "[object Number]":return a!=+a?c!=+c:a==0?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source== 9 | c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if(typeof a!="object"||typeof c!="object")return false;for(var f=d.length;f--;)if(d[f]==a)return true;d.push(a);var f=0,g=true;if(e=="[object Array]"){if(f=a.length,g=f==c.length)for(;f--;)if(!(g=f in a==f in c&&q(a[f],c[f],d)))break}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return false;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&q(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c, 10 | h)&&!f--)break;g=!f}}d.pop();return g}var r=this,G=r._,n={},k=Array.prototype,o=Object.prototype,i=k.slice,H=k.unshift,l=o.toString,I=o.hasOwnProperty,w=k.forEach,x=k.map,y=k.reduce,z=k.reduceRight,A=k.filter,B=k.every,C=k.some,p=k.indexOf,D=k.lastIndexOf,o=Array.isArray,J=Object.keys,s=Function.prototype.bind,b=function(a){return new m(a)};if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports)exports=module.exports=b;exports._=b}else r._=b;b.VERSION="1.3.1";var j=b.each= 11 | b.forEach=function(a,c,d){if(a!=null)if(w&&a.forEach===w)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e2;a== 12 | null&&(a=[]);if(y&&a.reduce===y)return e&&(c=b.bind(c,e)),f?a.reduce(c,d):a.reduce(c);j(a,function(a,b,i){f?d=c.call(e,d,a,b,i):(d=a,f=true)});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(z&&a.reduceRight===z)return e&&(c=b.bind(c,e)),f?a.reduceRight(c,d):a.reduceRight(c);var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect= 13 | function(a,c,b){var e;E(a,function(a,g,h){if(c.call(b,a,g,h))return e=a,true});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(A&&a.filter===A)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(B&&a.every===B)return a.every(c,b);j(a,function(a,g,h){if(!(e= 14 | e&&c.call(b,a,g,h)))return n});return e};var E=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(C&&a.some===C)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return n});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;return p&&a.indexOf===p?a.indexOf(c)!=-1:b=E(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck= 15 | function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a, 17 | c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1));return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}}; 24 | b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=J||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.defaults=function(a){j(i.call(arguments, 25 | 1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return q(a,b,[])};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=o||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)}; 26 | b.isArguments=function(a){return l.call(a)=="[object Arguments]"};if(!b.isArguments(arguments))b.isArguments=function(a){return!(!a||!b.has(a,"callee"))};b.isFunction=function(a){return l.call(a)=="[object Function]"};b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"}; 27 | b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,b){return I.call(a,b)};b.noConflict=function(){r._=G;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.mixin=function(a){j(b.functions(a), 28 | function(c){K(c,b[c]=a[c])})};var L=0;b.uniqueId=function(a){var b=L++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var t=/.^/,u=function(a){return a.replace(/\\\\/g,"\\").replace(/\\'/g,"'")};b.template=function(a,c){var d=b.templateSettings,d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.escape||t,function(a,b){return"',_.escape("+ 29 | u(b)+"),'"}).replace(d.interpolate||t,function(a,b){return"',"+u(b)+",'"}).replace(d.evaluate||t,function(a,b){return"');"+u(b).replace(/[\r\n\t]/g," ")+";__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",e=new Function("obj","_",d);return c?e(c,b):function(a){return e.call(this,a,b)}};b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var v=function(a,c){return c?b(a).chain():a},K=function(a,c){m.prototype[a]= 30 | function(){var a=i.call(arguments);H.call(a,this._wrapped);return v(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return v(d,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return v(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain= 31 | true;return this};m.prototype.value=function(){return this._wrapped}}).call(this); 32 | -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/genindex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Index — Simple calculator 1.0 documentation 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 | 32 |
33 | 34 | 35 |

Index

36 | 37 |
38 | 39 |
40 | 41 | 42 |
43 | 44 |
45 |
46 | 86 |
87 |
88 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Simple calculator’s documentation! — Simple calculator 1.0 documentation 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 | 32 |
33 | 34 |
35 |

Simple calculator’s documentation!

36 |
37 |
38 |
39 | 40 | 41 |
42 | 43 |
44 |
45 | 85 |
86 |
87 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/ch09-documentation/docs/source/_build/html/objects.inv -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/quickstart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Quickstart guide — Simple calculator 1.0 documentation 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 | 33 | 34 |
35 | 36 |
37 |

Quickstart guide

38 |

This quickstart guide explains how to use our calculator.

39 |
40 |

Examples

41 |
from calculator import Calculator
 42 | calculator = Calculator(5)  # Calculator(5)
 43 | calculator.add(10)  # Calculator(15)
 44 | calculator.multiply(10)  # Calculator(150)
 45 | 
46 |
47 |
48 |
49 | 50 | 51 |
52 | 53 |
54 |
55 | 106 |
107 |
108 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Search — Simple calculator 1.0 documentation 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 | 36 | 37 |
38 | 39 |

Search

40 |
41 | 42 |

43 | Please activate JavaScript to enable the search 44 | functionality. 45 |

46 |
47 |

48 | Searching for multiple words only shows matches that contain 49 | all words. 50 |

51 |
52 | 53 | 54 | 55 |
56 | 57 |
58 | 59 |
60 | 61 |
62 | 63 |
64 |
65 | 95 |
96 |
97 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/searchindex.js: -------------------------------------------------------------------------------- 1 | Search.setIndex({docnames:["index","quickstart","tutorials/tutorial1","tutorials/tutorial2"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":3,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":2,"sphinx.domains.rst":2,"sphinx.domains.std":1,sphinx:56},filenames:["index.rst","quickstart.rst","tutorials/tutorial1.rst","tutorials/tutorial2.rst"],objects:{},objnames:{},objtypes:{},terms:{"150":1,"import":1,add:1,adipisc:3,aliqua:3,aliquip:3,amet:3,anim:3,aut:3,calcul:1,cillum:3,commodo:3,consectetur:3,consequat:3,content:[],culpa:3,cupidatat:3,deserunt:3,dolor:3,dui:3,eiusmod:3,elit:3,enim:3,ess:3,est:3,exampl:[],excepteur:3,exercit:3,explain:1,from:1,fugiat:3,guid:[],how:1,incididunt:3,index:[],ipsum:3,irur:3,labor:3,labori:3,laborum:3,lorem:3,magna:3,minim:3,modul:[],mollit:3,multipli:1,nisi:3,non:3,nostrud:3,nulla:3,occaecat:3,officia:3,our:1,page:[],pariatur:3,profit:[],proident:3,qui:3,quickstart:[],reprehenderit:3,search:[],sed:3,sint:3,sit:3,step:2,sunt:3,tempor:3,thi:1,tutori:[],ullamco:3,use:1,velit:3,veniam:3,volupt:3},titles:["Simple calculator\u2019s documentation!","Quickstart guide","Tutorial 1","Tutorial 2"],titleterms:{calcul:0,document:0,exampl:1,guid:1,indic:[],quickstart:1,simpl:0,tabl:[],tutori:[2,3],welcom:[]}}) -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/tutorials/tutorial1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Tutorial 1 — Simple calculator 1.0 documentation 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 | 32 |
33 | 34 |
35 |

Tutorial 1

36 |
    37 |
  • Step 1

  • 38 |
  • Step 2

  • 39 |
  • Step 3

  • 40 |
41 |
42 | 43 | 44 |
45 | 46 |
47 |
48 | 88 |
89 |
90 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /ch09-documentation/docs/source/_build/html/tutorials/tutorial2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Tutorial 2 — Simple calculator 1.0 documentation 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 | 32 |
33 | 34 |
35 |

Tutorial 2

36 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

37 |
38 | 39 | 40 |
41 | 42 |
43 |
44 | 84 |
85 |
86 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /ch09-documentation/docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | Calculator 5 | ---------- 6 | 7 | .. automodule:: calculator 8 | :members: 9 | 10 | math_operations 11 | --------------- 12 | 13 | .. automodule:: math_operations 14 | :members: -------------------------------------------------------------------------------- /ch09-documentation/docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('../..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'Simple calculator' 21 | copyright = '2020, Sebastian Witowski' 22 | author = 'Sebastian Witowski' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '1.0' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.viewcode', 36 | 'sphinx.ext.doctest', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # List of patterns, relative to source directory, that match files and 43 | # directories to ignore when looking for source files. 44 | # This pattern also affects html_static_path and html_extra_path. 45 | exclude_patterns = [] 46 | 47 | 48 | # -- Options for HTML output ------------------------------------------------- 49 | 50 | # The theme to use for HTML and HTML Help pages. See the documentation for 51 | # a list of builtin themes. 52 | # 53 | html_theme = 'alabaster' 54 | 55 | # Add any paths that contain custom static files (such as style sheets) here, 56 | # relative to this directory. They are copied after the builtin static files, 57 | # so a file named "default.css" will overwrite the builtin "default.css". 58 | html_static_path = ['_static'] -------------------------------------------------------------------------------- /ch09-documentation/docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Simple calculator documentation master file, created by 2 | sphinx-quickstart 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Simple calculator's documentation! 7 | ============================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | quickstart.rst 14 | api.rst 15 | tutorials/tutorial1.rst 16 | tutorials/tutorial2.rst -------------------------------------------------------------------------------- /ch09-documentation/docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart guide 2 | ================ 3 | 4 | This quickstart guide explains how to use our calculator. 5 | 6 | Examples 7 | -------- 8 | .. code-block:: python 9 | 10 | from calculator import Calculator 11 | calculator = Calculator(5) # Calculator(5) 12 | calculator.add(10) # Calculator(15) 13 | calculator.multiply(10) # Calculator(150) 14 | -------------------------------------------------------------------------------- /ch09-documentation/docs/source/tutorials/tutorial1.rst: -------------------------------------------------------------------------------- 1 | Tutorial 1 2 | ========== 3 | 4 | * Step 1 5 | * Step 2 6 | * Step 3 -------------------------------------------------------------------------------- /ch09-documentation/docs/source/tutorials/tutorial2.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 2 | ========== 3 | 4 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -------------------------------------------------------------------------------- /ch09-documentation/math_operations.py: -------------------------------------------------------------------------------- 1 | def add(left, right): 2 | """Add two numbers. 3 | 4 | >>> from math_operations import add 5 | >>> add(2, 2) 6 | 4 7 | >>> add(-1, -1) 8 | -2 9 | 10 | :param left: augend (left operand) 11 | :param right: addend (right operand) 12 | :return: sum of left and right operands 13 | """ 14 | return left + right 15 | 16 | 17 | def subtract(left, right): 18 | """Subtract two numbers. 19 | 20 | >>> from math_operations import subtract 21 | >>> subtract(2, 2) 22 | 0 23 | >>> subtract(-3, -1) 24 | -2 25 | 26 | :param left: minuend (left operand) 27 | :param right: subtrahend (right operand) 28 | :return: difference between left and right operand 29 | """ 30 | return left - right 31 | 32 | 33 | def multiply(left, right): 34 | """Multiply two numbers. 35 | 36 | :param left: multiplicand (left operand) 37 | :param right: multiplier (right operand) 38 | :return: product of multiplication 39 | """ 40 | return left * right 41 | 42 | 43 | def divide(left, right): 44 | """Divide two numbers.""" 45 | return left // right 46 | -------------------------------------------------------------------------------- /ch10-ci/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v3.2.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - repo: https://github.com/psf/black 10 | rev: 19.3b0 11 | hooks: 12 | - id: black 13 | - repo: https://gitlab.com/pycqa/flake8 14 | rev: 3.8.4 15 | hooks: 16 | - id: flake8 17 | -------------------------------------------------------------------------------- /ch10-ci/calculator.py: -------------------------------------------------------------------------------- 1 | from math_operations import add, divide, multiply, subtract 2 | 3 | 4 | class Calculator: 5 | """Simple calculator with basic math operations.""" 6 | 7 | def __init__(self, value=0): 8 | """Initialize the value of the calculator.""" 9 | self.value = value 10 | 11 | def __repr__(self): 12 | """Return the current value.""" 13 | return f"Calculator({self.value})" 14 | 15 | def add(self, operand): 16 | """Add operand to the current value of the calculator. 17 | 18 | >>> from calculator import Calculator 19 | >>> calculator = Calculator(5) 20 | >>> calculator.add(10) 21 | Calculator(15) 22 | """ 23 | self.value = add(self.value, operand) 24 | return self 25 | 26 | def subtract(self, operand): 27 | self.value = subtract(self.value, operand) 28 | return self 29 | 30 | def multiply(self, operand): 31 | self.value = multiply(self.value, operand) 32 | return self 33 | 34 | def divide(self, operand): 35 | self.value = divide(self.value, operand) 36 | return self 37 | -------------------------------------------------------------------------------- /ch10-ci/math_operations.py: -------------------------------------------------------------------------------- 1 | def add(left, right): 2 | """Add two numbers. 3 | 4 | >>> from math_operations import add 5 | >>> add(2, 2) 6 | 4 7 | >>> add(-1, -1) 8 | -2 9 | 10 | :param left: augend (left operand) 11 | :param right: addend (right operand) 12 | :return: sum of left and right operands 13 | """ 14 | return left + right 15 | 16 | 17 | def subtract(left, right): 18 | """Subtract two numbers. 19 | 20 | >>> from math_operations import subtract 21 | >>> subtract(2, 2) 22 | 0 23 | >>> subtract(-3, -1) 24 | -2 25 | 26 | :param left: minuend (left operand) 27 | :param right: subtrahend (right operand) 28 | :return: difference between left and right operand 29 | """ 30 | return left - right 31 | 32 | 33 | def multiply(left, right): 34 | """Multiply two numbers. 35 | 36 | :param left: multiplicand (left operand) 37 | :param right: multiplier (right operand) 38 | :return: product of multiplication 39 | """ 40 | return left * right 41 | 42 | 43 | def divide(left, right): 44 | """Divide two numbers.""" 45 | return left // right 46 | -------------------------------------------------------------------------------- /ch10-ci/test_calculator.py: -------------------------------------------------------------------------------- 1 | from calculator import Calculator 2 | 3 | 4 | def test_calculator(): 5 | calc = Calculator() 6 | calc.add(10) 7 | calc.subtract(5) 8 | calc.multiply(5) 9 | assert str(calc) == "Calculator(25)" 10 | 11 | 12 | def test_calculator_initial_value(): 13 | calc = Calculator(10) 14 | assert str(calc) == "Calculator(10)" 15 | calc.add(5) 16 | assert str(calc) == "Calculator(15)" 17 | 18 | 19 | def test_chaining_operations(): 20 | calc = Calculator() 21 | calc.add(5).add(10).subtract(15) 22 | assert str(calc) == "Calculator(0)" 23 | -------------------------------------------------------------------------------- /ch10-ci/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36,py38 3 | skipsdist = true 4 | 5 | [testenv] 6 | # install pytest in the virtualenv where commands will be executed 7 | deps = 8 | pytest 9 | black 10 | flake8 11 | commands = 12 | # NOTE: you can run any command line tool here - not just tests 13 | pytest 14 | black . 15 | flake8 . 16 | -------------------------------------------------------------------------------- /ch11-cli/uptimer/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "restructuredtext.confPath": "${workspaceFolder}/source", 3 | "restructuredtext.languageServer.disabled": true 4 | } 5 | -------------------------------------------------------------------------------- /ch11-cli/uptimer/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /ch11-cli/uptimer/README.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/ch11-cli/uptimer/README.rst -------------------------------------------------------------------------------- /ch11-cli/uptimer/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /ch11-cli/uptimer/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "uptimer" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Sebastian Witowski "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.9" 9 | click = "^7.1.2" 10 | requests = "^2.25.0" 11 | colorama = "^0.4.4" 12 | pytest-mock = "^3.4.0" 13 | sphinx-click = "^2.5.0" 14 | 15 | [tool.poetry.dev-dependencies] 16 | pytest = "^6.2.0" 17 | Sphinx = "^3.3.1" 18 | 19 | [tool.poetry.scripts] 20 | uptimer = "uptimer.uptimer:check" 21 | 22 | [build-system] 23 | requires = ["poetry-core>=1.0.0"] 24 | build-backend = "poetry.core.masonry.api" 25 | -------------------------------------------------------------------------------- /ch11-cli/uptimer/source/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | 5 | Helper functions 6 | ~~~~~~~~~~~~~~~~ 7 | 8 | .. automodule:: uptimer.uptimer 9 | :members: 10 | .. autofunction:: check 11 | -------------------------------------------------------------------------------- /ch11-cli/uptimer/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("..")) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = "Uptimer" 22 | copyright = "2020, Sebastian Witowski" 23 | author = "Sebastian Witowski" 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = "1.0" 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx_click.ext"] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ["_templates"] 38 | 39 | # List of patterns, relative to source directory, that match files and 40 | # directories to ignore when looking for source files. 41 | # This pattern also affects html_static_path and html_extra_path. 42 | exclude_patterns = [] 43 | 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | 47 | # The theme to use for HTML and HTML Help pages. See the documentation for 48 | # a list of builtin themes. 49 | # 50 | html_theme = "alabaster" 51 | 52 | # Add any paths that contain custom static files (such as style sheets) here, 53 | # relative to this directory. They are copied after the builtin static files, 54 | # so a file named "default.css" will overwrite the builtin "default.css". 55 | html_static_path = ["_static"] 56 | -------------------------------------------------------------------------------- /ch11-cli/uptimer/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Uptimer documentation master file, created by 2 | sphinx-quickstart on Wed Dec 16 11:45:26 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Uptimer's documentation! 7 | =================================== 8 | 9 | Uptimer is a CLI tool to monitor the status of websites. 10 | Specify a URL, and it will return its HTTP status code. 11 | 12 | Quickstart 13 | ---------- 14 | 15 | 1. Install poetry (Uptimer uses `poetry `_): 16 | 17 | $ pip install poetry 18 | 19 | 2. Install dependencies 20 | 21 | $ pip install 22 | 23 | 3. Run uptimer 24 | 25 | $ poetry run uptimer https://www.website-to-check/ 26 | 27 | 28 | CLI commands 29 | ------------ 30 | 31 | .. click:: uptimer.uptimer:check 32 | :prog: uptimer 33 | 34 | 35 | Useful links 36 | ------------ 37 | 38 | .. toctree:: 39 | :maxdepth: 2 40 | 41 | api.rst 42 | -------------------------------------------------------------------------------- /ch11-cli/uptimer/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/ch11-cli/uptimer/tests/__init__.py -------------------------------------------------------------------------------- /ch11-cli/uptimer/tests/test_uptimer.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pytest 3 | import requests 4 | import sys 5 | from click.testing import CliRunner 6 | from uptimer import __version__ 7 | from uptimer.uptimer import check, check_url, colorize_status 8 | 9 | 10 | def test_version(): 11 | assert __version__ == "0.1.0" 12 | 13 | 14 | def mock_response_object(code): 15 | resp = requests.Response() 16 | resp.status_code = code 17 | return resp 18 | 19 | 20 | def test_check_url(mocker): 21 | mocker.patch("requests.head", return_value=mock_response_object(200)) 22 | assert check_url("dummyurl") == 200 23 | 24 | mocker.patch("requests.head", return_value=mock_response_object(404)) 25 | assert check_url("dummyurl") == 404 26 | 27 | with pytest.raises(TypeError): 28 | check_url() 29 | 30 | 31 | def test_colorize_status(mocker): 32 | mocker.patch("click.secho") 33 | # colorize_status("dummyurl", 200) 34 | # click.secho.assert_called() 35 | # Or to check if we called it with correct parameters: 36 | url = "dummyurl" 37 | status = 200 38 | colorize_status(url, status) 39 | click.secho.assert_called_with(f"{url} -> {status}", fg="green") 40 | 41 | 42 | @pytest.mark.skipif( 43 | sys.platform == "win32", reason="Testing colorized output doesn't work on Windows" 44 | ) 45 | @pytest.mark.parametrize( 46 | "code,color", 47 | [ 48 | (200, "green"), 49 | (304, "yellow"), 50 | (404, "bright_red"), 51 | (500, "red"), 52 | (1, "magenta"), 53 | ], 54 | ) 55 | def test_check_one_url(mocker, code, color): 56 | mocker.patch("requests.head", return_value=mock_response_object(code)) 57 | 58 | runner = CliRunner() 59 | result = runner.invoke(check, ["dummyurl"], color=True) 60 | 61 | expected_message = click.style(f"dummyurl -> {code}", fg=color) 62 | assert result.output == f"{expected_message}\n" 63 | 64 | 65 | @pytest.mark.skipif( 66 | sys.platform == "win32", reason="Testing colorized output doesn't work on Windows" 67 | ) 68 | def test_check_multiple_urls(mocker): 69 | mocker.patch( 70 | "requests.head", 71 | side_effect=[mock_response_object(200), mock_response_object(500)], 72 | ) 73 | 74 | runner = CliRunner() 75 | result = runner.invoke(check, ["dummyurl1", "dummyurl2"], color=True) 76 | 77 | expected_message1 = click.style("dummyurl1 -> 200", fg="green") 78 | expected_message2 = click.style("dummyurl2 -> 500", fg="red") 79 | assert result.output == f"{expected_message1}\n{expected_message2}\n" 80 | -------------------------------------------------------------------------------- /ch11-cli/uptimer/uptimer/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /ch11-cli/uptimer/uptimer/uptimer.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | import click 4 | import requests 5 | 6 | 7 | def check_url(url): 8 | """Send HEAD request to the url and return the HTTP status code 9 | 10 | :param url: URL to check 11 | :type url: str 12 | :return: HTTP status code 13 | :rtype: int 14 | """ 15 | try: 16 | response = requests.head(url) 17 | except requests.exceptions.ConnectionError: 18 | click.echo(f"ConnectionError: Can't reach {url} URL!") 19 | return None 20 | return response.status_code 21 | 22 | 23 | def colorize_status(url, status): 24 | """Print the URL and status in color to the terminal 25 | 26 | :param url: URL to print 27 | :type url: str 28 | :param status: status used to determine the color of the output 29 | :type status: str 30 | """ 31 | # fmt: off 32 | colors = { 33 | 2: "green", 34 | 3: "yellow", 35 | 4: "bright_red", 36 | 5: "red", 37 | } 38 | # fmt: on 39 | click.secho(f"{url} -> {status}", fg=colors.get(status // 100, "magenta")) 40 | 41 | 42 | @click.command() 43 | @click.argument("urls", nargs=-1, required=True) 44 | @click.option("--daemon", "-d", default=False, is_flag=True) 45 | def check(urls, daemon): 46 | """Check urls and print their HTTP statuses (with colors) 47 | 48 | :param urls: URL (or a tuple with multiple URLs) to check 49 | :type urls: str or tuple(str) 50 | :param daemon: If set to True, after checking all URLs, 51 | sleep for 5 seconds and check them again 52 | :type daemon: bool 53 | """ 54 | 55 | while True: 56 | for url in urls: 57 | status_code = check_url(url) 58 | if status_code: 59 | colorize_status(url, status_code) 60 | if not daemon: 61 | break 62 | sleep(5) 63 | 64 | 65 | if __name__ == "__main__": 66 | check() 67 | -------------------------------------------------------------------------------- /ch12-package/uptimer/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /ch12-package/uptimer/.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * Uptimer version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /ch12-package/uptimer/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | # IDE settings 105 | .vscode/ 106 | -------------------------------------------------------------------------------- /ch12-package/uptimer/AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Sebastian Witowski 9 | 10 | Contributors 11 | ------------ 12 | 13 | None yet. Why not be the first? 14 | -------------------------------------------------------------------------------- /ch12-package/uptimer/CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every little bit 8 | helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/switowski/uptimer/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 30 | wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | Uptimer could always use more documentation, whether as part of the 42 | official Uptimer docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/switowski/uptimer/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up `uptimer` for local development. 61 | 62 | 1. Fork the `uptimer` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/uptimer.git 66 | 67 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 68 | 69 | $ mkvirtualenv uptimer 70 | $ cd uptimer/ 71 | $ python setup.py develop 72 | 73 | 4. Create a branch for local development:: 74 | 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, check that your changes pass flake8 and the 80 | tests, including testing other Python versions with tox:: 81 | 82 | $ flake8 uptimer tests 83 | $ python setup.py test or pytest 84 | $ tox 85 | 86 | To get flake8 and tox, just pip install them into your virtualenv. 87 | 88 | 6. Commit your changes and push your branch to GitHub:: 89 | 90 | $ git add . 91 | $ git commit -m "Your detailed description of your changes." 92 | $ git push origin name-of-your-bugfix-or-feature 93 | 94 | 7. Submit a pull request through the GitHub website. 95 | 96 | Pull Request Guidelines 97 | ----------------------- 98 | 99 | Before you submit a pull request, check that it meets these guidelines: 100 | 101 | 1. The pull request should include tests. 102 | 2. If the pull request adds functionality, the docs should be updated. Put 103 | your new functionality into a function with a docstring, and add the 104 | feature to the list in README.rst. 105 | 3. The pull request should work for Python 3.5, 3.6, 3.7 and 3.8, and for PyPy. Check 106 | https://travis-ci.com/switowski/uptimer/pull_requests 107 | and make sure that the tests pass for all supported Python versions. 108 | 109 | Tips 110 | ---- 111 | 112 | To run a subset of tests:: 113 | 114 | $ pytest tests.test_uptimer 115 | 116 | 117 | Deploying 118 | --------- 119 | 120 | A reminder for the maintainers on how to deploy. 121 | Make sure all your changes are committed (including an entry in HISTORY.rst). 122 | Then run:: 123 | 124 | $ bump2version patch # possible: major / minor / patch 125 | $ git push 126 | $ git push --tags 127 | 128 | Travis will then deploy to PyPI if tests pass. 129 | -------------------------------------------------------------------------------- /ch12-package/uptimer/HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | 0.1.0 (2020-12-16) 6 | ------------------ 7 | 8 | * First release on PyPI. 9 | -------------------------------------------------------------------------------- /ch12-package/uptimer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020, Sebastian Witowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ch12-package/uptimer/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | 7 | recursive-include tests * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 12 | -------------------------------------------------------------------------------- /ch12-package/uptimer/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | from urllib.request import pathname2url 8 | 9 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 10 | endef 11 | export BROWSER_PYSCRIPT 12 | 13 | define PRINT_HELP_PYSCRIPT 14 | import re, sys 15 | 16 | for line in sys.stdin: 17 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 18 | if match: 19 | target, help = match.groups() 20 | print("%-20s %s" % (target, help)) 21 | endef 22 | export PRINT_HELP_PYSCRIPT 23 | 24 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 25 | 26 | help: 27 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 28 | 29 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 30 | 31 | clean-build: ## remove build artifacts 32 | rm -fr build/ 33 | rm -fr dist/ 34 | rm -fr .eggs/ 35 | find . -name '*.egg-info' -exec rm -fr {} + 36 | find . -name '*.egg' -exec rm -f {} + 37 | 38 | clean-pyc: ## remove Python file artifacts 39 | find . -name '*.pyc' -exec rm -f {} + 40 | find . -name '*.pyo' -exec rm -f {} + 41 | find . -name '*~' -exec rm -f {} + 42 | find . -name '__pycache__' -exec rm -fr {} + 43 | 44 | clean-test: ## remove test and coverage artifacts 45 | rm -fr .tox/ 46 | rm -f .coverage 47 | rm -fr htmlcov/ 48 | rm -fr .pytest_cache 49 | 50 | lint: ## check style with flake8 51 | flake8 uptimer tests 52 | 53 | test: ## run tests quickly with the default Python 54 | pytest 55 | 56 | test-all: ## run tests on every Python version with tox 57 | tox 58 | 59 | coverage: ## check code coverage quickly with the default Python 60 | coverage run --source uptimer -m pytest 61 | coverage report -m 62 | coverage html 63 | $(BROWSER) htmlcov/index.html 64 | 65 | docs: ## generate Sphinx HTML documentation, including API docs 66 | rm -f docs/uptimer.rst 67 | rm -f docs/modules.rst 68 | sphinx-apidoc -o docs/ uptimer 69 | $(MAKE) -C docs clean 70 | $(MAKE) -C docs html 71 | $(BROWSER) docs/_build/html/index.html 72 | 73 | servedocs: docs ## compile the docs watching for changes 74 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 75 | 76 | release: dist ## package and upload a release 77 | twine upload dist/* 78 | 79 | dist: clean ## builds source and wheel package 80 | python setup.py sdist 81 | python setup.py bdist_wheel 82 | ls -l dist 83 | 84 | install: clean ## install the package to the active Python's site-packages 85 | python setup.py install 86 | -------------------------------------------------------------------------------- /ch12-package/uptimer/README.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Uptimer 3 | ======= 4 | 5 | 6 | .. image:: https://img.shields.io/pypi/v/uptimer.svg 7 | :target: https://pypi.python.org/pypi/uptimer 8 | 9 | .. image:: https://img.shields.io/travis/switowski/uptimer.svg 10 | :target: https://travis-ci.com/switowski/uptimer 11 | 12 | .. image:: https://readthedocs.org/projects/uptimer/badge/?version=latest 13 | :target: https://uptimer.readthedocs.io/en/latest/?badge=latest 14 | :alt: Documentation Status 15 | 16 | 17 | 18 | 19 | CLI tool to ping websites and return their HTTP codes. 20 | 21 | 22 | * Free software: MIT license 23 | * Documentation: https://uptimer.readthedocs.io. 24 | 25 | 26 | Features 27 | -------- 28 | 29 | * TODO 30 | 31 | Credits 32 | ------- 33 | 34 | This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template. 35 | 36 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter 37 | .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage 38 | -------------------------------------------------------------------------------- /ch12-package/uptimer/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = uptimer 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /ch12-package/uptimer/docs/api.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | API 3 | ===== 4 | 5 | .. automodule:: uptimer.helpers 6 | :members: 7 | 8 | .. click:: uptimer.cli:main 9 | :prog: uptimer -------------------------------------------------------------------------------- /ch12-package/uptimer/docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /ch12-package/uptimer/docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # uptimer documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Jun 9 13:47:02 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another 16 | # directory, add these directories to sys.path here. If the directory is 17 | # relative to the documentation root, use os.path.abspath to make it 18 | # absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | 23 | sys.path.insert(0, os.path.abspath("..")) 24 | 25 | import uptimer 26 | 27 | # -- General configuration --------------------------------------------- 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | # 31 | # needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 35 | extensions = [ 36 | "sphinx.ext.autodoc", 37 | "sphinx.ext.viewcode", 38 | "sphinx_click.ext", 39 | ] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ["_templates"] 43 | 44 | # The suffix(es) of source filenames. 45 | # You can specify multiple suffix as a list of string: 46 | # 47 | # source_suffix = ['.rst', '.md'] 48 | source_suffix = ".rst" 49 | 50 | # The master toctree document. 51 | master_doc = "index" 52 | 53 | # General information about the project. 54 | project = "Uptimer" 55 | copyright = "2020, Sebastian Witowski" 56 | author = "Sebastian Witowski" 57 | 58 | # The version info for the project you're documenting, acts as replacement 59 | # for |version| and |release|, also used in various other places throughout 60 | # the built documents. 61 | # 62 | # The short X.Y version. 63 | version = uptimer.__version__ 64 | # The full version, including alpha/beta/rc tags. 65 | release = uptimer.__version__ 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = None 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | # This patterns also effect to html_static_path and html_extra_path 77 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 78 | 79 | # The name of the Pygments (syntax highlighting) style to use. 80 | pygments_style = "sphinx" 81 | 82 | # If true, `todo` and `todoList` produce output, else they produce nothing. 83 | todo_include_todos = False 84 | 85 | 86 | # -- Options for HTML output ------------------------------------------- 87 | 88 | # The theme to use for HTML and HTML Help pages. See the documentation for 89 | # a list of builtin themes. 90 | # 91 | html_theme = "alabaster" 92 | 93 | # Theme options are theme-specific and customize the look and feel of a 94 | # theme further. For a list of options available for each theme, see the 95 | # documentation. 96 | # 97 | # html_theme_options = {} 98 | 99 | # Add any paths that contain custom static files (such as style sheets) here, 100 | # relative to this directory. They are copied after the builtin static files, 101 | # so a file named "default.css" will overwrite the builtin "default.css". 102 | html_static_path = ["_static"] 103 | 104 | 105 | # -- Options for HTMLHelp output --------------------------------------- 106 | 107 | # Output file base name for HTML help builder. 108 | htmlhelp_basename = "uptimerdoc" 109 | 110 | 111 | # -- Options for LaTeX output ------------------------------------------ 112 | 113 | latex_elements = { 114 | # The paper size ('letterpaper' or 'a4paper'). 115 | # 116 | # 'papersize': 'letterpaper', 117 | # The font size ('10pt', '11pt' or '12pt'). 118 | # 119 | # 'pointsize': '10pt', 120 | # Additional stuff for the LaTeX preamble. 121 | # 122 | # 'preamble': '', 123 | # Latex figure (float) alignment 124 | # 125 | # 'figure_align': 'htbp', 126 | } 127 | 128 | # Grouping the document tree into LaTeX files. List of tuples 129 | # (source start file, target name, title, author, documentclass 130 | # [howto, manual, or own class]). 131 | latex_documents = [ 132 | (master_doc, "uptimer.tex", "Uptimer Documentation", "Sebastian Witowski", "manual") 133 | ] 134 | 135 | 136 | # -- Options for manual page output ------------------------------------ 137 | 138 | # One entry per manual page. List of tuples 139 | # (source start file, name, description, authors, manual section). 140 | man_pages = [(master_doc, "uptimer", "Uptimer Documentation", [author], 1)] 141 | 142 | 143 | # -- Options for Texinfo output ---------------------------------------- 144 | 145 | # Grouping the document tree into Texinfo files. List of tuples 146 | # (source start file, target name, title, author, 147 | # dir menu entry, description, category) 148 | texinfo_documents = [ 149 | ( 150 | master_doc, 151 | "uptimer", 152 | "Uptimer Documentation", 153 | author, 154 | "uptimer", 155 | "One line description of project.", 156 | "Miscellaneous", 157 | ) 158 | ] 159 | -------------------------------------------------------------------------------- /ch12-package/uptimer/docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /ch12-package/uptimer/docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst 2 | -------------------------------------------------------------------------------- /ch12-package/uptimer/docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Uptimer's documentation! 2 | ====================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | readme 9 | installation 10 | usage 11 | api 12 | contributing 13 | authors 14 | history 15 | 16 | Indices and tables 17 | ================== 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /ch12-package/uptimer/docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | 8 | Stable release 9 | -------------- 10 | 11 | To install Uptimer, run this command in your terminal: 12 | 13 | .. code-block:: console 14 | 15 | $ pip install uptimer 16 | 17 | This is the preferred method to install Uptimer, as it will always install the most recent stable release. 18 | 19 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide 20 | you through the process. 21 | 22 | .. _pip: https://pip.pypa.io 23 | .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ 24 | 25 | 26 | From sources 27 | ------------ 28 | 29 | The sources for Uptimer can be downloaded from the `Github repo`_. 30 | 31 | You can either clone the public repository: 32 | 33 | .. code-block:: console 34 | 35 | $ git clone git://github.com/switowski/uptimer 36 | 37 | Or download the `tarball`_: 38 | 39 | .. code-block:: console 40 | 41 | $ curl -OJL https://github.com/switowski/uptimer/tarball/master 42 | 43 | Once you have a copy of the source, you can install it with: 44 | 45 | .. code-block:: console 46 | 47 | $ python setup.py install 48 | 49 | 50 | .. _Github repo: https://github.com/switowski/uptimer 51 | .. _tarball: https://github.com/switowski/uptimer/tarball/master 52 | -------------------------------------------------------------------------------- /ch12-package/uptimer/docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=uptimer 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /ch12-package/uptimer/docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /ch12-package/uptimer/docs/usage.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Quickstart 3 | ========== 4 | 5 | To use Uptimer in your terminal:: 6 | 7 | $ uptimer https://www.website-to-check.com 8 | -------------------------------------------------------------------------------- /ch12-package/uptimer/requirements_dev.in: -------------------------------------------------------------------------------- 1 | bump2version 2 | wheel 3 | watchdog 4 | flake8 5 | tox 6 | coverage 7 | Sphinx 8 | twine 9 | Click 10 | pytest 11 | pytest-runner 12 | pytest-mock 13 | sphinx-click -------------------------------------------------------------------------------- /ch12-package/uptimer/requirements_dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile requirements_dev.in 6 | # 7 | alabaster==0.7.12 8 | # via sphinx 9 | appdirs==1.4.4 10 | # via virtualenv 11 | attrs==20.3.0 12 | # via pytest 13 | babel==2.9.1 14 | # via sphinx 15 | bleach==3.3.0 16 | # via readme-renderer 17 | bump2version==1.0.1 18 | # via -r requirements_dev.in 19 | certifi==2022.12.7 20 | # via requests 21 | cffi==1.14.5 22 | # via cryptography 23 | chardet==4.0.0 24 | # via requests 25 | click==7.1.2 26 | # via -r requirements_dev.in 27 | colorama==0.4.4 28 | # via twine 29 | coverage==5.3 30 | # via -r requirements_dev.in 31 | cryptography==39.0.1 32 | # via secretstorage 33 | distlib==0.3.1 34 | # via virtualenv 35 | docutils==0.16 36 | # via 37 | # readme-renderer 38 | # sphinx 39 | filelock==3.0.12 40 | # via 41 | # tox 42 | # virtualenv 43 | flake8==3.8.4 44 | # via -r requirements_dev.in 45 | idna==2.10 46 | # via requests 47 | imagesize==1.2.0 48 | # via sphinx 49 | iniconfig==1.1.1 50 | # via pytest 51 | jeepney==0.6.0 52 | # via 53 | # keyring 54 | # secretstorage 55 | jinja2==2.11.3 56 | # via sphinx 57 | keyring==21.5.0 58 | # via twine 59 | markupsafe==1.1.1 60 | # via jinja2 61 | mccabe==0.6.1 62 | # via flake8 63 | packaging==20.8 64 | # via 65 | # bleach 66 | # pytest 67 | # sphinx 68 | # tox 69 | pbr==5.5.1 70 | # via sphinx-click 71 | pkginfo==1.6.1 72 | # via twine 73 | pluggy==0.13.1 74 | # via 75 | # pytest 76 | # tox 77 | py==1.10.0 78 | # via 79 | # pytest 80 | # tox 81 | pycodestyle==2.6.0 82 | # via flake8 83 | pycparser==2.20 84 | # via cffi 85 | pyflakes==2.2.0 86 | # via flake8 87 | pygments==2.7.4 88 | # via 89 | # readme-renderer 90 | # sphinx 91 | pyparsing==2.4.7 92 | # via packaging 93 | pytest==6.2.1 94 | # via 95 | # -r requirements_dev.in 96 | # pytest-mock 97 | pytest-mock==3.4.0 98 | # via -r requirements_dev.in 99 | pytest-runner==5.2 100 | # via -r requirements_dev.in 101 | pytz==2020.4 102 | # via babel 103 | readme-renderer==28.0 104 | # via twine 105 | requests==2.25.1 106 | # via 107 | # requests-toolbelt 108 | # sphinx 109 | # twine 110 | requests-toolbelt==0.9.1 111 | # via twine 112 | rfc3986==1.4.0 113 | # via twine 114 | secretstorage==3.3.1 115 | # via keyring 116 | six==1.15.0 117 | # via 118 | # bleach 119 | # readme-renderer 120 | # tox 121 | # virtualenv 122 | snowballstemmer==2.0.0 123 | # via sphinx 124 | sphinx==3.3.1 125 | # via 126 | # -r requirements_dev.in 127 | # sphinx-click 128 | sphinx-click==2.5.0 129 | # via -r requirements_dev.in 130 | sphinxcontrib-applehelp==1.0.2 131 | # via sphinx 132 | sphinxcontrib-devhelp==1.0.2 133 | # via sphinx 134 | sphinxcontrib-htmlhelp==1.0.3 135 | # via sphinx 136 | sphinxcontrib-jsmath==1.0.1 137 | # via sphinx 138 | sphinxcontrib-qthelp==1.0.3 139 | # via sphinx 140 | sphinxcontrib-serializinghtml==1.1.4 141 | # via sphinx 142 | toml==0.10.2 143 | # via 144 | # pytest 145 | # tox 146 | tox==3.20.1 147 | # via -r requirements_dev.in 148 | tqdm==4.54.1 149 | # via twine 150 | twine==3.2.0 151 | # via -r requirements_dev.in 152 | urllib3==1.26.5 153 | # via requests 154 | virtualenv==20.2.2 155 | # via tox 156 | watchdog==1.0.1 157 | # via -r requirements_dev.in 158 | webencodings==0.5.1 159 | # via bleach 160 | wheel==0.38.1 161 | # via -r requirements_dev.in 162 | 163 | # The following packages are considered to be unsafe in a requirements file: 164 | # setuptools 165 | -------------------------------------------------------------------------------- /ch12-package/uptimer/setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:uptimer/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | exclude = docs 19 | 20 | [aliases] 21 | # Define setup.py command aliases here 22 | test = pytest 23 | -------------------------------------------------------------------------------- /ch12-package/uptimer/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """The setup script.""" 4 | 5 | from setuptools import setup, find_packages 6 | 7 | with open("README.rst") as readme_file: 8 | readme = readme_file.read() 9 | 10 | with open("HISTORY.rst") as history_file: 11 | history = history_file.read() 12 | 13 | requirements = [ 14 | "Click>=7.0", 15 | "requests>=2.25", 16 | "colorama>=0.4", 17 | ] 18 | 19 | setup_requirements = ["pytest-runner"] 20 | 21 | test_requirements = ["pytest>=3"] 22 | 23 | setup( 24 | author="Sebastian Witowski", 25 | author_email="sebastian@switowski.com", 26 | python_requires=">=3.5", 27 | classifiers=[ 28 | "Development Status :: 2 - Pre-Alpha", 29 | "Intended Audience :: Developers", 30 | "License :: OSI Approved :: MIT License", 31 | "Natural Language :: English", 32 | "Programming Language :: Python :: 3", 33 | "Programming Language :: Python :: 3.5", 34 | "Programming Language :: Python :: 3.6", 35 | "Programming Language :: Python :: 3.7", 36 | "Programming Language :: Python :: 3.8", 37 | ], 38 | description="CLI tool to ping websites and return their HTTP codes.", 39 | entry_points={"console_scripts": ["uptimer=uptimer.cli:main"]}, 40 | install_requires=requirements, 41 | license="MIT license", 42 | long_description=readme + "\n\n" + history, 43 | include_package_data=True, 44 | keywords="uptimer", 45 | name="uptimer", 46 | packages=find_packages(include=["uptimer", "uptimer.*"]), 47 | setup_requires=setup_requirements, 48 | test_suite="tests", 49 | tests_require=test_requirements, 50 | url="https://github.com/switowski/uptimer", 51 | version="0.1.0", 52 | zip_safe=False, 53 | ) 54 | -------------------------------------------------------------------------------- /ch12-package/uptimer/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit test package for uptimer.""" 2 | -------------------------------------------------------------------------------- /ch12-package/uptimer/tests/test_uptimer.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pytest 3 | import requests 4 | from click.testing import CliRunner 5 | from uptimer import __version__ 6 | from uptimer.cli import main 7 | from uptimer.helpers import check_url, colorize_status 8 | 9 | 10 | def test_version(): 11 | assert __version__ == "0.1.0" 12 | 13 | 14 | def mock_response_object(code): 15 | resp = requests.Response() 16 | resp.status_code = code 17 | return resp 18 | 19 | 20 | def test_check_url(mocker): 21 | mocker.patch("requests.head", return_value=mock_response_object(200)) 22 | assert check_url("dummyurl") == 200 23 | 24 | mocker.patch("requests.head", return_value=mock_response_object(404)) 25 | assert check_url("dummyurl") == 404 26 | 27 | with pytest.raises(TypeError): 28 | check_url() 29 | 30 | 31 | def test_colorize_status(mocker): 32 | mocker.patch("click.secho") 33 | # colorize_status("dummyurl", 200) 34 | # click.secho.assert_called() 35 | # Or to check if we called it with correct parameters: 36 | url = "dummyurl" 37 | status = 200 38 | colorize_status(url, status) 39 | click.secho.assert_called_with(f"{url} -> {status}", fg="green") 40 | 41 | 42 | @pytest.mark.parametrize( 43 | "code,color", 44 | [ 45 | (200, "green"), 46 | (304, "yellow"), 47 | (404, "bright_red"), 48 | (500, "red"), 49 | (1, "magenta"), 50 | ], 51 | ) 52 | def test_check_one_url(mocker, code, color): 53 | mocker.patch("requests.head", return_value=mock_response_object(code)) 54 | 55 | runner = CliRunner() 56 | result = runner.invoke(main, ["dummyurl"], color=True) 57 | 58 | expected_message = click.style(f"dummyurl -> {code}", fg=color) 59 | assert result.output == f"{expected_message}\n" 60 | 61 | 62 | def test_check_multiple_urls(mocker): 63 | mocker.patch( 64 | "requests.head", 65 | side_effect=[mock_response_object(200), mock_response_object(500)], 66 | ) 67 | 68 | runner = CliRunner() 69 | result = runner.invoke(main, ["dummyurl1", "dummyurl2"], color=True) 70 | 71 | expected_message1 = click.style("dummyurl1 -> 200", fg="green") 72 | expected_message2 = click.style("dummyurl2 -> 500", fg="red") 73 | assert result.output == f"{expected_message1}\n{expected_message2}\n" 74 | -------------------------------------------------------------------------------- /ch12-package/uptimer/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py35, py36, py37, py38, flake8 3 | 4 | [travis] 5 | python = 6 | 3.8: py38 7 | 3.7: py37 8 | 3.6: py36 9 | 3.5: py35 10 | 11 | [testenv:flake8] 12 | basepython = python 13 | deps = flake8 14 | commands = flake8 uptimer tests 15 | 16 | [testenv] 17 | setenv = 18 | PYTHONPATH = {toxinidir} 19 | deps = 20 | -r{toxinidir}/requirements_dev.txt 21 | ; If you want to make tox run the tests with the same versions, create a 22 | ; requirements.txt with the pinned versions and uncomment the following line: 23 | ; -r{toxinidir}/requirements.txt 24 | commands = 25 | pip install -U pip 26 | pytest --basetemp={envtmpdir} 27 | -------------------------------------------------------------------------------- /ch12-package/uptimer/uptimer/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for Uptimer.""" 2 | 3 | __author__ = """Sebastian Witowski""" 4 | __email__ = "sebastian@switowski.com" 5 | __version__ = "0.1.0" 6 | -------------------------------------------------------------------------------- /ch12-package/uptimer/uptimer/cli.py: -------------------------------------------------------------------------------- 1 | """Console script for uptimer.""" 2 | import sys 3 | from time import sleep 4 | 5 | import click 6 | 7 | from uptimer.helpers import check_url, colorize_status 8 | 9 | 10 | @click.command() 11 | @click.argument("urls", nargs=-1, required=True) 12 | @click.option("--daemon", "-d", default=False, is_flag=True) 13 | def main(urls, daemon): 14 | """Check urls and print their HTTP statuses (with colors) 15 | 16 | :param urls: URL (or a tuple with multiple URLs) to check 17 | :type urls: str or tuple(str) 18 | :param daemon: If set to True, after checking all URLs, 19 | sleep for 5 seconds and check them again 20 | :type daemon: bool 21 | """ 22 | 23 | while True: 24 | for url in urls: 25 | status_code = check_url(url) 26 | if status_code: 27 | colorize_status(url, status_code) 28 | if not daemon: 29 | break 30 | sleep(5) 31 | 32 | 33 | if __name__ == "__main__": 34 | sys.exit(main()) # pragma: no cover 35 | -------------------------------------------------------------------------------- /ch12-package/uptimer/uptimer/helpers.py: -------------------------------------------------------------------------------- 1 | import click 2 | import requests 3 | 4 | 5 | def check_url(url): 6 | """Send HEAD request to the url and return the HTTP status code 7 | 8 | :param url: URL to check 9 | :type url: str 10 | :return: HTTP status code 11 | :rtype: int 12 | """ 13 | try: 14 | response = requests.head(url) 15 | except requests.exceptions.ConnectionError: 16 | click.echo(f"ConnectionError: Can't reach {url} URL!") 17 | return None 18 | return response.status_code 19 | 20 | 21 | def colorize_status(url, status): 22 | """Print the URL and status in color to the terminal 23 | 24 | :param url: URL to print 25 | :type url: str 26 | :param status: status used to determine the color of the output 27 | :type status: str 28 | """ 29 | # fmt: off 30 | colors = { 31 | 2: "green", 32 | 3: "yellow", 33 | 4: "bright_red", 34 | 5: "red", 35 | } 36 | # fmt: on 37 | click.secho(f"{url} -> {status}", fg=colors.get(status // 100, "magenta")) 38 | -------------------------------------------------------------------------------- /ch13-executable/gui.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/ch13-executable/gui.exe -------------------------------------------------------------------------------- /ch13-executable/guptimer/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for Guptimer.""" 2 | 3 | __version__ = "0.1.0" 4 | -------------------------------------------------------------------------------- /ch13-executable/guptimer/gui.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | from guptimer.helpers import check_url 4 | 5 | # Dictionary to map statuses to color tags 6 | COLORS = { 7 | 2: "green", 8 | 3: "yellow", 9 | 4: "bright_red", 10 | 5: "red", 11 | } 12 | 13 | 14 | def main(): 15 | """Draw a GUI for checking URLs""" 16 | 17 | def check_urls(): 18 | # Grab text from URLs box and split it 19 | urls_string = urls_box.get("1.0", tk.END) 20 | urls = urls_string.rstrip().split("\n") 21 | # Remove the "disabled" state from the response box, so we can edit it 22 | response_box.configure(state="normal") 23 | # Remove all current content 24 | response_box.delete("1.0", tk.END) 25 | for line, url in enumerate(urls, start=1): 26 | status_code = check_url(url) 27 | if status_code: 28 | # Write HTTP status next to each URL ... 29 | response_box.insert(tk.END, str(status_code) + "\n") 30 | # ... and colorize it 31 | fg_color = COLORS.get(status_code // 100, "magenta") 32 | response_box.tag_add(fg_color, f"{line}.0", f"{line}.9") 33 | else: 34 | response_box.insert(tk.END, "Wrong URL!\n") 35 | response_box.tag_add("magenta", f"{line}.0", f"{line}.9") 36 | # Disable the response box, so users can't edit it 37 | response_box.configure(state="disabled") 38 | 39 | # Create a new window 40 | window = tk.Tk() 41 | # Add gray background 42 | window.config(bg="#f6f6f6") 43 | 44 | # Add label in the first row 45 | tk.Label(window, text="URLs to check (one per line)").grid(row=0) 46 | 47 | # Add a text box where users can input URLs 48 | urls_box = tk.Text(window, height=20, width=50) 49 | urls_box.grid(row=1, column=0) 50 | 51 | # Add a "disabled" textbox where we display response codes 52 | response_box = tk.Text( 53 | window, 54 | height=20, 55 | width=10, 56 | state="disabled", 57 | bg="#f6f6f6" 58 | ) 59 | response_box.grid(row=1, column=1) 60 | 61 | # Tags can be used to change text color in a text box 62 | response_box.tag_config("green", foreground="#9CCC65") 63 | response_box.tag_config("yellow", foreground="#FF9800") 64 | response_box.tag_config("bright_red", foreground="#EF5350") 65 | response_box.tag_config("red", foreground="#C62828") 66 | response_box.tag_config("magenta", foreground="#9C27B0") 67 | 68 | # Add a button and connect it to a check_urls function 69 | check_button = tk.Button(window, text="Check", command=check_urls) 70 | check_button.grid(row=2) 71 | 72 | # Start main event loop, so our program will respond to button clicks 73 | window.mainloop() 74 | 75 | 76 | # Test URLs: 77 | # http://httpstat.us/200 78 | # http://httpstat.us/301 79 | # http://httpstat.us/404 80 | # http://httpstat.us/500 81 | 82 | if __name__ == "__main__": 83 | main() 84 | -------------------------------------------------------------------------------- /ch13-executable/guptimer/helpers.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def check_url(url): 5 | """Send HEAD request to the url and return the HTTP status code 6 | 7 | :param url: URL to check 8 | :type url: str 9 | :return: HTTP status code 10 | :rtype: int 11 | """ 12 | try: 13 | response = requests.head(url) 14 | except requests.exceptions.ConnectionError: 15 | return None 16 | return response.status_code 17 | -------------------------------------------------------------------------------- /ch13-executable/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """The setup script.""" 4 | 5 | from setuptools import setup 6 | 7 | requirements = [ 8 | "requests>=2.25", 9 | ] 10 | 11 | setup( 12 | author="Sebastian Witowski", 13 | author_email="sebastian@switowski.com", 14 | python_requires=">=3.8", 15 | classifiers=[ 16 | "Development Status :: 2 - Pre-Alpha", 17 | "Intended Audience :: Developers", 18 | "License :: OSI Approved :: MIT License", 19 | "Natural Language :: English", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | ], 24 | description="GUI tool to ping websites and return their HTTP codes.", 25 | entry_points={ 26 | "console_scripts": [ 27 | "guptimer=guptimer.gui:main", 28 | ] 29 | }, 30 | install_requires=requirements, 31 | license="MIT license", 32 | keywords="guptimer", 33 | name="guptimer", 34 | packages=["guptimer"], 35 | url="https://github.com/switowski/guptimer", 36 | version="0.1.0", 37 | zip_safe=False, 38 | ) 39 | -------------------------------------------------------------------------------- /ch14-deployment/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/Users/tutorials/modern-python-projects/ch14-deployment/.venv/bin/python3.9" 3 | } -------------------------------------------------------------------------------- /ch14-deployment/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Debian Buster with Python 3.9.1 2 | FROM python:3.9.1-buster 3 | 4 | # Copy requirements first to leverage Docker caching 5 | COPY requirements.txt . 6 | 7 | # Install pip packages 8 | RUN pip install -r requirements.txt 9 | 10 | # Create and use a new user, so we don't use "root" user 11 | RUN useradd --create-home --shell /bin/bash app 12 | WORKDIR /home/app 13 | USER app 14 | 15 | # IMPORTANT: At the beginning of this chapter, I use port 80 and then switch to 8000 16 | # Make sure you use the right port below! 17 | # Expose port 80 <-- This is optional, but it's a good practice 18 | EXPOSE 80 19 | 20 | # Copy the rest of the code inside the container 21 | COPY . . 22 | 23 | # Start gunicorn server with 3 workers, uvicorn worker type and use the 0.0.0.0 host with port 80 24 | ENTRYPOINT ["gunicorn", "-w", "3", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:80", "main:app"] 25 | -------------------------------------------------------------------------------- /ch14-deployment/Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn -w 3 -k uvicorn.workers.UvicornWorker main:app 2 | -------------------------------------------------------------------------------- /ch14-deployment/helpers.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | 3 | 4 | async def get_status(url: str) -> int: 5 | async with httpx.AsyncClient() as client: 6 | try: 7 | resp = await client.head(url, allow_redirects=False) 8 | return resp.status_code 9 | except (httpx.ConnectError, httpx.UnsupportedProtocol): 10 | # Probably the URL was wrong 11 | return 0 12 | except httpx.ReadTimeout: 13 | # Timeout 14 | return -1 15 | -------------------------------------------------------------------------------- /ch14-deployment/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from fastapi import FastAPI, Form, Request 4 | from fastapi.templating import Jinja2Templates 5 | 6 | from helpers import get_status 7 | 8 | app = FastAPI() 9 | 10 | templates = Jinja2Templates(directory="templates") 11 | 12 | # Dictionary mapping the first digit of a status code to Bootstrap colors 13 | COLORS = { 14 | 1: "primary", # Information responses 15 | 2: "success", # Successful responses 16 | 3: "info", # Redirection responses 17 | 4: "warning", # Client errors 18 | 5: "danger", # Server errors 19 | 0: "dark", # Special case for wrong URL 20 | -1: "dark", # Special case for timeout 21 | } 22 | 23 | 24 | @app.get("/") 25 | async def home(request: Request): 26 | return templates.TemplateResponse("home.html", {"request": request}) 27 | 28 | 29 | @app.post("/check/") 30 | async def check(request: Request, urls: str = Form(...)): 31 | urls_list = urls.split() 32 | 33 | tasks = [get_status(url) for url in urls_list] 34 | statuses = await asyncio.gather(*tasks) 35 | 36 | return templates.TemplateResponse( 37 | "home.html", 38 | {"request": request, "statuses": statuses, "colors": COLORS, "urls": urls}, 39 | ) 40 | 41 | # Test URLs: 42 | # http://httpstat.us/200 43 | # http://httpstat.us/301 44 | # http://httpstat.us/404 45 | # http://httpstat.us/500 46 | 47 | # http://httpstat.us/200?sleep=3000 48 | # http://httpstat.us/301?sleep=3000 49 | # http://httpstat.us/404?sleep=3000 50 | # http://httpstat.us/500?sleep=3000 -------------------------------------------------------------------------------- /ch14-deployment/requirements.in: -------------------------------------------------------------------------------- 1 | fastapi # Web framework 2 | uvicorn[standard] # Dev web server and workers for production server 3 | jinja2 # Templating language 4 | python-multipart # Required to use forms with FastAPI 5 | httpx # Perform asynchronous HTTP requests 6 | gunicorn # Production web server 7 | -------------------------------------------------------------------------------- /ch14-deployment/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | anyio==3.6.1 8 | # via httpcore 9 | asgiref==3.4.1 10 | # via uvicorn 11 | certifi==2022.12.7 12 | # via 13 | # httpcore 14 | # httpx 15 | click==7.1.2 16 | # via uvicorn 17 | fastapi==0.65.2 18 | # via -r requirements.in 19 | gunicorn==20.0.4 20 | # via -r requirements.in 21 | h11==0.11.0 22 | # via 23 | # httpcore 24 | # uvicorn 25 | httpcore==0.15.0 26 | # via httpx 27 | httptools==0.2.0 28 | # via uvicorn 29 | httpx==0.23.0 30 | # via -r requirements.in 31 | idna==2.10 32 | # via 33 | # anyio 34 | # rfc3986 35 | jinja2==2.11.3 36 | # via -r requirements.in 37 | markupsafe==1.1.1 38 | # via jinja2 39 | pydantic==1.7.4 40 | # via fastapi 41 | python-dotenv==0.15.0 42 | # via uvicorn 43 | python-multipart==0.0.5 44 | # via -r requirements.in 45 | pyyaml==5.4 46 | # via uvicorn 47 | rfc3986[idna2008]==1.4.0 48 | # via httpx 49 | six==1.15.0 50 | # via python-multipart 51 | sniffio==1.2.0 52 | # via 53 | # anyio 54 | # httpcore 55 | # httpx 56 | starlette==0.14.2 57 | # via fastapi 58 | uvicorn[standard]==0.14.0 59 | # via -r requirements.in 60 | uvloop==0.14.0 61 | # via uvicorn 62 | watchgod==0.6 63 | # via uvicorn 64 | websockets==9.1 65 | # via uvicorn 66 | 67 | # The following packages are considered to be unsafe in a requirements file: 68 | # setuptools 69 | -------------------------------------------------------------------------------- /ch14-deployment/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Uptimer web 7 | 8 | 9 | 10 |
11 |
12 |
13 |

Uptimer online:

14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 |

Responses:

22 | {% if statuses %} 23 |
    24 | {% for status in statuses %} 25 |
  • 26 | 27 | {% if status == 0 %} 28 | Wrong URL 29 | {% elif status == -1 %} 30 | Timeout 31 | {% else %} 32 | {{ status }} 33 | {% endif %} 34 | 35 |
  • 36 | {% endfor %} 37 |
38 | {% endif %} 39 |
40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 |
49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /guides/pyenv_vscode/README.md: -------------------------------------------------------------------------------- 1 | # Making `pyenv-win` work with VS Code in Windows 2 | 3 | At the time of recording this course, VS Code has some problems with discovering Python versions installed with `pyenv-win`. However, there is an experimental feature that solves this issues, so maybe if you are watching it in the future, you won't have this problem. 4 | 
 5 | So, if you are a Windows user and you don't see Python versions installed with `pyenv-win` when you run the "Python: Select Interpreter" command, follow the instruction from [this GitHub issue](https://github.com/microsoft/vscode-python/issues/15304): 6 | 7 | - Open the VS Code settings 8 | - Search for *Python > Experiments: Opt Into* option and click *Edit in settings.json*: 9 | 10 | ![](https://raw.githubusercontent.com/talkpython/modern-python-projects-course/master/guides/pyenv_vscode/resources/1-experiments-opt-into.png) 11 | 12 | - This will open your settings file. Inside the "python.experiments.optInto" add "pythonDiscoveryModule", so it looks like this: 13 | 14 | ![](https://raw.githubusercontent.com/talkpython/modern-python-projects-course/master/guides/pyenv_vscode/resources/2-pythondiscoverymodule.png) 15 | 16 | - Reload VS Code 17 | - Now, when you run "Python: Select Interpreter", you should see Python versions installed with `pyenv-win`: 18 | 19 | ![](https://raw.githubusercontent.com/talkpython/modern-python-projects-course/master/guides/pyenv_vscode/resources/3-python-select-interpreter.png) 20 | 21 | **Important**: If you don't see the "Python > Experiments: Opt Into" option, make sure you use the **latest version of VS Code** and that you set up `pyenv-win` correctly, including **setting up the PYENV environment variables** as explained in the installation instructions: [https://github.com/pyenv-win/pyenv-win#finish-the-installation](https://github.com/pyenv-win/pyenv-win#finish-the-installation) 22 | 23 | ![](https://raw.githubusercontent.com/talkpython/modern-python-projects-course/master/guides/pyenv_vscode/resources/4-environment-variables.png) 24 | 25 | If that still doesn't work, there is the hard way of pointing VS Code to a specific Python version: 26 | 27 | - Run the "Python: Select Interpreter" command 28 | - Select "I can't find the interpreter I want to select ..." 29 | - Manually browse to the python.exe file installed with `pyenv-win`. By default it should be inside `.pyenv\pyenv-win\versions\\python.exe` (for me it was `c:\Users\switowski\.pyenv\pyenv-win\versions\3.8.1\python.exe`): 30 | 31 | ![](https://raw.githubusercontent.com/talkpython/modern-python-projects-course/master/guides/pyenv_vscode/resources/5-i-can-t-find-the-interpreter.png) 32 | -------------------------------------------------------------------------------- /guides/pyenv_vscode/resources/1-experiments-opt-into.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/guides/pyenv_vscode/resources/1-experiments-opt-into.png -------------------------------------------------------------------------------- /guides/pyenv_vscode/resources/2-pythondiscoverymodule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/guides/pyenv_vscode/resources/2-pythondiscoverymodule.png -------------------------------------------------------------------------------- /guides/pyenv_vscode/resources/3-python-select-interpreter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/guides/pyenv_vscode/resources/3-python-select-interpreter.png -------------------------------------------------------------------------------- /guides/pyenv_vscode/resources/4-environment-variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/guides/pyenv_vscode/resources/4-environment-variables.png -------------------------------------------------------------------------------- /guides/pyenv_vscode/resources/5-i-can-t-find-the-interpreter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/guides/pyenv_vscode/resources/5-i-can-t-find-the-interpreter.png -------------------------------------------------------------------------------- /readme_resources/course-img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-python-projects-course/b7c25db9d7411f58d6af17358883843b80fa2a71/readme_resources/course-img.jpg --------------------------------------------------------------------------------