├── .gitignore
├── README.md
├── section-1
├── __init__.py
├── c_lib
│ ├── Nature-View.jpg
│ ├── __init__.py
│ ├── c_lib.py
│ ├── c_lib_refactored.py
│ └── histogram_module
│ │ ├── include
│ │ └── opencv_histogram.hpp
│ │ ├── setup.py
│ │ └── src
│ │ ├── histogram.cpp
│ │ └── opencv_histogram.cpp
├── cpuperf
│ ├── cpuperf.py
│ └── cpuperf_refactored.py
├── ioperf
│ ├── ioperf.py
│ └── ioperf_refactored.py
├── pypy
│ ├── Nature-View.jpg
│ └── pypy.py
└── requirements.txt
├── section-2
├── docker
│ ├── Dockerfile
│ ├── main.py
│ ├── requirements.txt
│ └── workdir
│ │ ├── Dockerfile
│ │ ├── main.py
│ │ └── requirements.txt
├── setuptools
│ ├── LICENSE
│ ├── MANIFEST.in
│ ├── README.md
│ ├── flickr_downloader
│ │ ├── __init__.py
│ │ └── flickr_downloader.py
│ └── setup.py
└── standalone
│ ├── main.py
│ ├── main_windowed.py
│ └── requirements.txt
├── section-3
├── bdd
│ ├── requirements.txt
│ ├── src
│ │ ├── flickr_downloader
│ │ │ ├── __init__.py
│ │ │ └── flickr_downloader.py
│ │ └── setup.py
│ └── tests
│ │ ├── __init__.py
│ │ ├── bdd
│ │ ├── __init__.py
│ │ ├── download_interesting_photos.feature
│ │ ├── get_photo_list.feature
│ │ ├── test_download_interesting_photos.py
│ │ └── test_get_photo_list.py
│ │ ├── integration
│ │ ├── __init__.py
│ │ └── test_flickr_downloader.py
│ │ └── test_flickr_downloader.py
├── integration
│ ├── requirements.txt
│ ├── src
│ │ ├── flickr_downloader
│ │ │ ├── __init__.py
│ │ │ └── flickr_downloader.py
│ │ └── setup.py
│ └── tests
│ │ ├── __init__.py
│ │ ├── integration
│ │ ├── __init__.py
│ │ └── test_flickr_downloader.py
│ │ └── test_flickr_downloader.py
├── tdd
│ ├── flickr_downloader
│ │ ├── __init__.py
│ │ └── flickr_downloader.py
│ ├── requirements.txt
│ ├── specific_flickr_downloader
│ │ ├── __init__.py
│ │ └── specific_flickr_downloader.py
│ └── tests
│ │ ├── __init__.py
│ │ └── test_specific_flickr_downloader.py
└── unit
│ ├── requirements.txt
│ ├── src
│ ├── flickr_downloader
│ │ ├── __init__.py
│ │ └── flickr_downloader.py
│ └── setup.py
│ └── tests
│ ├── __init__.py
│ └── test_flickr_downloader.py
├── section-4
├── debugging
│ └── histogram
│ │ ├── Nature-View.jpg
│ │ └── pypy.py
├── envs
│ ├── .gitignore
│ ├── README.md
│ └── histogram
│ │ ├── Nature-View.jpg
│ │ └── pypy.py
├── pip
│ ├── main.py
│ └── requirements.txt
└── prototyping
│ ├── requests_library.ipynb
│ └── sine_wave_plot.ipynb
└── section-5
├── lint
├── flickr_downloader
│ ├── __init__.py
│ └── flickr_downloader.py
└── pylintrc
├── static-code-analysis
├── flickr_downloader
│ ├── __init__.py
│ └── flickr_downloader.py
├── pylintrc
├── requirements.txt
├── setup.py
├── sonar-project.properties
└── tests
│ ├── __init__.py
│ ├── integration
│ ├── __init__.py
│ └── test_flickr_downloader.py
│ └── test_flickr_downloader.py
└── types
├── flickr_downloader
├── __init__.py
└── flickr_downloader.py
├── pylintrc
├── requirements.txt
├── setup.py
├── sonar-project.properties
└── tests
├── __init__.py
├── integration
├── __init__.py
└── test_flickr_downloader.py
└── test_flickr_downloader.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 | *.o
9 |
10 | # Distribution / packaging
11 | .Python
12 | env/
13 | build/
14 | develop-eggs/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | *.egg-info/
23 | .installed.cfg
24 | *.egg
25 | dist/
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | .pytest_cache/
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *,cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 |
56 | # Sphinx documentation
57 | docs/_build/
58 |
59 | # PyBuilder
60 | target/
61 |
62 | # Virtual Env
63 | venv/
64 |
65 | # Debug config files
66 | debug_config.cfg
67 |
68 | # Bower
69 | bower_components/
70 |
71 | # Node
72 | node_modules/
73 |
74 | # Sass
75 | .sass-cache
76 |
77 | # Jetbrains
78 | .idea/
79 |
80 | # Project files
81 | *downloaded/
82 | .DS_Store
83 | profiling_results
84 | workdir/
85 | .ipynb_checkpoints/
86 | .scannerwork/
87 | junit-result.xml
88 | .mypy_cache/
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tips, Tricks, and Techniques for Python Application Development [Video]
2 | This is the code repository for [Tips, Tricks, and Techniques for Python Application Development [Video]](https://www.packtpub.com/application-development/tips-tricks-and-techniques-python-application-development-video?utm_source=github&utm_medium=repository&utm_campaign=9781789139235), published by [Packt](https://www.packtpub.com/?utm_source=github). It contains all the supporting project files necessary to work through the video course from start to finish.
3 | ## About the Video Course
4 | The course starts off by dealing with performance issues and learning how to tackle them in an application. Distributing an application with Python is not easy but you will learn ways to distribute applications developed using Python along with GUIs, web applications, and more.
5 |
6 |
What You Will Learn
7 |
8 |
9 | - Use different tips, tricks, and techniques while developing Python application.
10 |
- Encounter performance issues in application and diagnose it.
11 |
- Explore most popular ways to distribute Python applications.
12 |
- Know the tools and formats to distribute packages, command-line, GUI and web applications.
13 |
- Use modern software and test application in the development process.
14 |
- Expand the productivity by using both standard and third-party tools.
15 |
- Manage code quality in order to find potential errors early in the development cycle.
16 |
17 | ## Instructions and Navigation
18 | ### Assumed Knowledge
19 | To fully benefit from the coverage included in this course, you will need:
20 | Knowledge of building applications with Python and Python programming knowledge and using it in their projects.
21 | ### Technical Requirements
22 | This course has the following software requirements:
23 | Minimum Hardware Requirements:
24 |
25 | For successful completion of this course, students will require the computer systems with at least the following:
26 |
27 | OS: Any modern OS (Windows, Mac, or Linux)
28 |
29 | RAM: minimum required for the Operating System, plus ~512 MB for the Docker virtual machine
30 |
31 | Storage: 1-3 GB free space to install and run Docker (size varies among operating systems), plus ~100MB free space for the Azure CLI, modules & scripts
32 |
33 | Software Requirements:
34 | A Java IDE to follow along with the examples (eg. Eclipse, NetBeans, IntelliJ IDEA)
35 |
36 | Docker (The Docker for Developers / Community Edition option) - https://www.docker.com/get-started
37 |
38 |
39 |
40 | ## Related Products
41 | * [Real World Projects with Vue.js [Video]](https://www.packtpub.com/web-development/real-world-projects-vuejs-video?utm_source=github&utm_medium=repository&utm_campaign=9781789340754)
42 |
43 | * [Java EE 8 Application Development [Video]](https://www.packtpub.com/application-development/java-ee-8-application-development-video?utm_source=github&utm_medium=repository&utm_campaign=9781788622189)
44 |
45 | * [Design Patterns in TypeScript [Video]](https://www.packtpub.com/application-development/design-patterns-typescript-video?utm_source=github&utm_medium=repository&utm_campaign=9781789347951)
46 |
47 |
--------------------------------------------------------------------------------
/section-1/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-1/__init__.py
--------------------------------------------------------------------------------
/section-1/c_lib/Nature-View.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-1/c_lib/Nature-View.jpg
--------------------------------------------------------------------------------
/section-1/c_lib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-1/c_lib/__init__.py
--------------------------------------------------------------------------------
/section-1/c_lib/c_lib.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import numpy
3 | import os
4 |
5 |
6 | def calculate_histogram(photo_):
7 | bins = numpy.zeros(256, numpy.int32)
8 |
9 | for i in range(photo_.shape[0]):
10 | for j in range(photo_.shape[1]):
11 | intensity = sum(photo_[i][j])
12 | bins[int(intensity/3)] += 1
13 |
14 | return bins
15 |
16 |
17 | if __name__ == "__main__":
18 | # Photo source: https://commons.wikimedia.org/wiki/File:Nature-View.jpg
19 | photo = cv2.imread(os.path.join(os.path.realpath(os.path.dirname(__file__)), "Nature-View.jpg"))
20 | histogram = calculate_histogram(photo)
21 | print(histogram)
22 |
--------------------------------------------------------------------------------
/section-1/c_lib/c_lib_refactored.py:
--------------------------------------------------------------------------------
1 | import os
2 | from histogram import *
3 |
4 |
5 | if __name__ == "__main__":
6 | # Photo source: https://commons.wikimedia.org/wiki/File:Nature-View.jpg
7 | print([int(i) for i in calculate_histogram(os.path.join(os.path.realpath(os.path.dirname(__file__)), "Nature-View.jpg"))])
8 |
9 |
--------------------------------------------------------------------------------
/section-1/c_lib/histogram_module/include/opencv_histogram.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | cv::Mat calculateOpenCVHistogram(const char *photoFile);
7 |
8 |
--------------------------------------------------------------------------------
/section-1/c_lib/histogram_module/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | import numpy
3 | from setuptools import setup, Extension
4 |
5 | setup(
6 | name = "histogram_wrapper",
7 | version = "1.0.0",
8 | ext_modules = [Extension("histogram",
9 | ["src/histogram.cpp", "src/opencv_histogram.cpp"],
10 | libraries=["opencv_core", "opencv_imgproc", "opencv_imgcodecs"],
11 | include_dirs=[numpy.get_include(),
12 | os.path.dirname(os.path.realpath(__file__)) + "/include"])]
13 | )
14 |
--------------------------------------------------------------------------------
/section-1/c_lib/histogram_module/src/histogram.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | #include "opencv_histogram.hpp"
6 |
7 | static PyObject *calculateHistogram(PyObject *self, PyObject *args) {
8 | const char *photoFile;
9 |
10 | if (!PyArg_ParseTuple(args, "s", &photoFile)) {
11 | return NULL;
12 | }
13 |
14 | cv::Mat histogram;
15 | histogram = calculateOpenCVHistogram(photoFile);
16 |
17 | npy_intp dims[1];
18 | dims[0] = histogram.rows * histogram.cols;
19 |
20 | PyObject *pyArray = PyArray_SimpleNewFromData(1, dims, NPY_FLOAT, reinterpret_cast(histogram.ptr(0)));
21 | PyArrayObject *npyArray = reinterpret_cast(pyArray);
22 |
23 | return PyArray_Return(npyArray);
24 |
25 | }
26 |
27 | static PyMethodDef module_methods[] = {
28 | {"calculate_histogram", calculateHistogram, METH_VARARGS, "Calculate histogram using OpenCV C++"},
29 | {NULL, NULL, 0, NULL}
30 | };
31 |
32 | static struct PyModuleDef histogram_module = {
33 | PyModuleDef_HEAD_INIT,
34 | "histogram",
35 | NULL,
36 | -1,
37 | module_methods
38 | };
39 |
40 | PyMODINIT_FUNC PyInit_histogram(void) {
41 | PyObject *module = PyModule_Create(&histogram_module);
42 | import_array();
43 | return module;
44 | }
45 |
46 |
47 |
--------------------------------------------------------------------------------
/section-1/c_lib/histogram_module/src/opencv_histogram.cpp:
--------------------------------------------------------------------------------
1 | #include "opencv_histogram.hpp"
2 |
3 | cv::Mat calculateOpenCVHistogram(const char *photoFile) {
4 | cv::Mat photo = cv::imread(photoFile);
5 | cv::Mat histogram;
6 | int channels[] = { 0 };
7 | int histSize[] = { 256 };
8 | float range[] = { 0, 256 };
9 | const float* ranges[] = { range };
10 |
11 | cv::Mat channel[3];
12 | cv::split(photo, channel);
13 |
14 | cv::Mat acc(photo.size(), CV_64F, cv::Scalar(0));
15 | accumulate(channel[0], acc);
16 | accumulate(channel[1], acc);
17 | accumulate(channel[2], acc);
18 | cv::Mat avg;
19 |
20 | acc.convertTo(avg, CV_8U, 1.0/3);
21 |
22 | cv::calcHist(&avg, 1, channels, cv::Mat(), histogram, 1, histSize, ranges, true, false);
23 |
24 | histogram = histogram.t();
25 |
26 | return histogram;
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/section-1/cpuperf/cpuperf.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import numpy
3 | import os
4 |
5 |
6 | def calculate_histogram(photo_):
7 | bins = numpy.zeros(256, numpy.int32)
8 |
9 | for i in range(photo_.shape[0]):
10 | for j in range(photo_.shape[1]):
11 | bins[photo_[i][j]] += 1
12 |
13 | return bins
14 |
15 |
16 | if __name__ == "__main__":
17 | photo_directory = os.path.join(os.path.realpath(os.path.dirname(__file__)), "..", "downloaded")
18 |
19 | photo_list = [cv2.imread(x.path, cv2.IMREAD_GRAYSCALE) for x in os.scandir(photo_directory) if
20 | x.path.endswith(".jpg")]
21 | for photo in photo_list:
22 | calculate_histogram(photo)
23 |
--------------------------------------------------------------------------------
/section-1/cpuperf/cpuperf_refactored.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import numpy
3 | import os
4 | from multiprocessing import Pool
5 |
6 |
7 | def calculate_histogram(photo_):
8 | bins = numpy.zeros(256, numpy.int32)
9 |
10 | for i in range(photo_.shape[0]):
11 | for j in range(photo_.shape[1]):
12 | bins[photo_[i][j]] += 1
13 |
14 | return bins
15 |
16 |
17 | if __name__ == "__main__":
18 | photo_directory = os.path.join(os.path.realpath(os.path.dirname(__file__)), "..", "downloaded")
19 |
20 | pool = Pool(8)
21 |
22 | photo_list = [cv2.imread(x.path, cv2.IMREAD_GRAYSCALE) for x in os.scandir(photo_directory) if x.path.endswith(".jpg")]
23 | pool.map(calculate_histogram, photo_list)
24 |
25 |
--------------------------------------------------------------------------------
/section-1/ioperf/ioperf.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import os
3 | import logging
4 |
5 |
6 | logger = logging.getLogger(__name__)
7 | logger.setLevel(logging.DEBUG)
8 |
9 |
10 | def get_photo_list():
11 | url = "https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=%s&format=json&nojsoncallback=1" % os.environ["FLICKR_API_KEY"]
12 |
13 | res = requests.get(url).json()
14 |
15 | return res
16 |
17 |
18 | def download_photo(_photo):
19 | photo_url = "https://farm%s.staticflickr.com/%s/%s_%s.jpg" % (_photo["farm"], _photo["server"], _photo["id"], _photo["secret"])
20 |
21 | photo_res = requests.get(photo_url)
22 |
23 | return photo_res.content
24 |
25 |
26 | def save_photo(_photo, _photo_content):
27 | with open(os.path.join(os.path.realpath(os.path.dirname(__file__)), "..", "downloaded", _photo["id"] + ".jpg"), "wb") as photo_file:
28 | photo_file.write(_photo_content)
29 |
30 |
31 | if __name__ == "__main__":
32 | photos_list_res = get_photo_list()
33 |
34 | photo_list = photos_list_res["photos"]["photo"]
35 |
36 | for photo in photo_list:
37 | photo_content = download_photo(photo)
38 | save_photo(photo, photo_content)
39 |
40 |
--------------------------------------------------------------------------------
/section-1/ioperf/ioperf_refactored.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import os
3 | import logging
4 | from concurrent.futures import ThreadPoolExecutor, as_completed
5 |
6 |
7 | logger = logging.getLogger(__name__)
8 | logger.setLevel(logging.DEBUG)
9 |
10 |
11 | def get_photo_list():
12 | url = "https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=%s&format=json&nojsoncallback=1" % os.environ["FLICKR_API_KEY"]
13 |
14 | res = requests.get(url).json()
15 |
16 | return res
17 |
18 |
19 | def download_photo(_photo):
20 | photo_url = "https://farm%s.staticflickr.com/%s/%s_%s.jpg" % (_photo["farm"], _photo["server"], _photo["id"], _photo["secret"])
21 |
22 | photo_res = requests.get(photo_url)
23 |
24 | return photo_res.content
25 |
26 |
27 | def save_photo(_photo, _photo_content):
28 | with open(os.path.join(os.path.realpath(os.path.dirname(__file__)), "..", "downloaded", _photo["id"] + ".jpg"), "wb") as photo_file:
29 | photo_file.write(_photo_content)
30 |
31 |
32 | if __name__ == "__main__":
33 | photos_list_res = get_photo_list()
34 |
35 | photo_list = photos_list_res["photos"]["photo"]
36 |
37 | with ThreadPoolExecutor() as e:
38 | future_photos = {e.submit(download_photo, photo): photo for photo in photo_list}
39 |
40 | for future in as_completed(future_photos):
41 | photo_content = future.result()
42 | save_photo(future_photos[future], photo_content)
43 |
44 |
--------------------------------------------------------------------------------
/section-1/pypy/Nature-View.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-1/pypy/Nature-View.jpg
--------------------------------------------------------------------------------
/section-1/pypy/pypy.py:
--------------------------------------------------------------------------------
1 | import os
2 | from PIL import Image
3 |
4 |
5 | def calculate_histogram(photo_):
6 | bins = []
7 | for i in range(256):
8 | bins.append(0)
9 |
10 | width, height = photo_.size
11 |
12 | for i in range(width):
13 | for j in range(height):
14 | intensity = sum(photo.getpixel((i, j)))
15 | bins[int(intensity/3)] += 1
16 |
17 | return bins
18 |
19 |
20 | if __name__ == "__main__":
21 | # Photo source: https://commons.wikimedia.org/wiki/File:Nature-View.jpg
22 | photo = Image.open(os.path.join(os.path.realpath(os.path.dirname(__file__)), "Nature-View.jpg"))
23 | histogram = calculate_histogram(photo)
24 | print(histogram)
25 |
--------------------------------------------------------------------------------
/section-1/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 | opencv-python
--------------------------------------------------------------------------------
/section-2/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3
2 |
3 | RUN mkdir -p /usr/app
4 | WORKDIR /usr/app
5 |
6 | ADD requirements.txt /usr/app
7 |
8 | RUN pip install -r requirements.txt
9 |
10 | ADD . /usr/app
11 |
12 | EXPOSE 8000
13 |
14 | CMD [ "gunicorn", "-b", "0.0.0.0:8000", "main:app" ]
15 |
16 |
17 |
--------------------------------------------------------------------------------
/section-2/docker/main.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 |
3 | app = Flask(__name__)
4 |
5 |
6 | @app.route("/")
7 | def hello():
8 | return "Hello World!"
9 |
10 |
11 | if __name__ == "__main__":
12 | app.run(host="0.0.0.0")
13 |
14 |
--------------------------------------------------------------------------------
/section-2/docker/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | gunicorn
3 |
--------------------------------------------------------------------------------
/section-2/docker/workdir/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3
2 |
3 | RUN mkdir -p /usr/app
4 | WORKDIR /usr/app
5 |
6 | ADD requirements.txt /usr/app
7 | RUN pip install -r requirements.txt
8 |
9 | ADD . /usr/app
10 |
11 | EXPOSE 8000
12 |
13 | CMD [ "gunicorn", "-b", "0.0.0.0:8000", "main:app" ]
14 |
15 |
--------------------------------------------------------------------------------
/section-2/docker/workdir/main.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 |
3 | app = Flask(__name__)
4 |
5 |
6 | @app.route("/")
7 | def hello():
8 | return "Hello World!"
9 |
10 |
11 | if __name__ == "__main__":
12 | app.run(host="0.0.0.0")
13 |
14 |
--------------------------------------------------------------------------------
/section-2/docker/workdir/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | gunicorn
3 |
4 |
--------------------------------------------------------------------------------
/section-2/setuptools/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Mihai Costea
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 |
--------------------------------------------------------------------------------
/section-2/setuptools/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.md *.txt LICENSE
2 |
--------------------------------------------------------------------------------
/section-2/setuptools/README.md:
--------------------------------------------------------------------------------
1 | # Flickr Downloader
2 |
3 | Flickr Downloader is a small Python package that can be used to download 100 interesting photos from Flickr.
4 |
5 |
6 | ## Installation
7 |
8 | ```
9 | $ pip install flickr_downloader
10 | ```
11 |
12 | ## Usage
13 |
14 | This package requires a valid Flickr API Key. You can get one from [Flickr Developer Guide](https://www.flickr.com/services/developer/api/)
15 |
16 |
17 | ```
18 | from flickr_downloader import download_interesting_photos
19 |
20 |
21 | flickr_api_key = ...
22 | location = ... # Download folder must already exist
23 |
24 | download_interesting_photos(flickr_api_key, location)
25 | ```
26 |
27 | ## License
28 | Flickr Downloader is released under the MIT license. See LICENSE for details.
29 |
30 | ## Contact
31 | Follow me on twitter [@mcostea](https://twitter.com/mcostea)
32 |
--------------------------------------------------------------------------------
/section-2/setuptools/flickr_downloader/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-2/setuptools/flickr_downloader/__init__.py
--------------------------------------------------------------------------------
/section-2/setuptools/flickr_downloader/flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import os
3 | import logging
4 | from concurrent.futures import ThreadPoolExecutor, as_completed
5 |
6 |
7 | logger = logging.getLogger(__name__)
8 | logger.setLevel(logging.DEBUG)
9 |
10 |
11 | def __get_photo_list(api_key):
12 | url = "https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=%s&format=json&nojsoncallback=1" % api_key
13 |
14 | res = requests.get(url).json()
15 |
16 | return res
17 |
18 |
19 | def __download_photo(_photo):
20 | photo_url = "https://farm%s.staticflickr.com/%s/%s_%s.jpg" % (_photo["farm"], _photo["server"], _photo["id"], _photo["secret"])
21 |
22 | photo_res = requests.get(photo_url)
23 |
24 | return photo_res.content
25 |
26 |
27 | def __save_photo(_photo, _photo_content, location):
28 | with open(os.path.join(location, _photo["id"] + ".jpg"), "wb") as photo_file:
29 | photo_file.write(_photo_content)
30 |
31 |
32 | def download_interesting_photos(flickr_api_key, location):
33 | photos_list_res = __get_photo_list(flickr_api_key)
34 |
35 | photo_list = photos_list_res["photos"]["photo"]
36 |
37 | with ThreadPoolExecutor() as e:
38 | future_photos = {e.submit(__download_photo, photo): photo for photo in photo_list}
39 |
40 | for future in as_completed(future_photos):
41 | photo_content = future.result()
42 | __save_photo(future_photos[future], photo_content, location)
43 |
--------------------------------------------------------------------------------
/section-2/setuptools/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 |
4 | with open("README.md", "r") as readme_file:
5 | long_description = readme_file.read()
6 |
7 | setup(
8 | name="flickr_downloader",
9 | version="1.0.0-dev2",
10 | description="Python package that downloads 100 interesting photos from Flickr",
11 | long_description=long_description,
12 | long_description_content_type="text/markdown",
13 | author="Mihai Costea",
14 | license="MIT",
15 | classifiers=[
16 | "Development Status :: 4 - Beta",
17 | "Intended Audience :: Developers",
18 | "License :: OSI Approved :: MIT License"
19 | ],
20 | keywords="flickr photo downloader",
21 | packages=find_packages(),
22 | install_requires=["requests>=2"],
23 | python_requires="~=3.5"
24 | )
25 |
--------------------------------------------------------------------------------
/section-2/standalone/main.py:
--------------------------------------------------------------------------------
1 | import os
2 | from flickr_downloader.flickr_downloader import download_interesting_photos
3 |
4 |
5 | if __name__ == "__main__":
6 | if not os.path.exists("downloaded"):
7 | os.mkdir("downloaded")
8 | download_interesting_photos(os.environ["FLICKR_API_KEY"], "downloaded")
9 |
--------------------------------------------------------------------------------
/section-2/standalone/main_windowed.py:
--------------------------------------------------------------------------------
1 | # You need PyGObject to run this application
2 | # See install instructions at https://pygobject.readthedocs.io/en/latest/getting_started.html
3 |
4 | import gi
5 | import os
6 | gi.require_version("Gtk", "3.0")
7 |
8 | from gi.repository import Gtk
9 | from flickr_downloader.flickr_downloader import download_interesting_photos
10 |
11 |
12 | def download_button_clicked(sender):
13 | write_path = os.path.join(os.path.expanduser("~"), "downloaded_photos")
14 | if not os.path.exists(write_path):
15 | os.mkdir(write_path)
16 | download_interesting_photos(os.environ["FLICKR_API_KEY"], write_path)
17 |
18 |
19 | window = Gtk.Window(title="Hello World")
20 |
21 | button = Gtk.Button(label="Download Photos")
22 |
23 | window.add(button)
24 |
25 | button.connect("clicked", download_button_clicked)
26 |
27 | window.show_all()
28 | window.connect("destroy", Gtk.main_quit)
29 | Gtk.main()
30 |
--------------------------------------------------------------------------------
/section-2/standalone/requirements.txt:
--------------------------------------------------------------------------------
1 | PyInstaller
2 |
--------------------------------------------------------------------------------
/section-3/bdd/requirements.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | pytest-mock
3 | coverage
4 | pytest-bdd
--------------------------------------------------------------------------------
/section-3/bdd/src/flickr_downloader/__init__.py:
--------------------------------------------------------------------------------
1 | from .flickr_downloader import FlickrDownloader, FlickrDownloaderException
2 |
--------------------------------------------------------------------------------
/section-3/bdd/src/flickr_downloader/flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import os
3 | import logging
4 | from concurrent.futures import ThreadPoolExecutor, as_completed
5 | from requests import RequestException
6 |
7 | logger = logging.getLogger(__name__)
8 | logger.setLevel(logging.DEBUG)
9 |
10 |
11 | class FlickrDownloaderException(Exception):
12 | pass
13 |
14 |
15 | class FlickrDownloader:
16 | def __init__(self, api_key, download_location):
17 | if not isinstance(api_key, str):
18 | raise FlickrDownloaderException("api_key should be str")
19 |
20 | if not isinstance(download_location, str):
21 | raise FlickrDownloaderException("download_location should be str")
22 |
23 | if api_key is None or len(api_key) == 0:
24 | raise FlickrDownloaderException("API Key is not set")
25 |
26 | if not os.path.exists(download_location) or not os.path.isdir(download_location):
27 | raise FlickrDownloaderException("Download location does not exist or is not a directory")
28 |
29 | self.__api_key = api_key
30 | self.__download_location = download_location
31 |
32 | def get_photo_list(self):
33 | url = "https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=%s&format=json&nojsoncallback=1" % self.__api_key
34 |
35 | try:
36 | res = requests.get(url).json()
37 | except RequestException as e:
38 | raise FlickrDownloaderException("Request error: %s", e)
39 |
40 | return res
41 |
42 | @staticmethod
43 | def download_photo(photo):
44 | try:
45 | photo_url = "https://farm%s.staticflickr.com/%s/%s_%s.jpg" % (photo["farm"], photo["server"], photo["id"], photo["secret"])
46 | except KeyError as e:
47 | raise FlickrDownloaderException("photo argument is invalid: %s" % e)
48 |
49 | try:
50 | photo_res = requests.get(photo_url)
51 | except RequestException as e:
52 | raise FlickrDownloaderException("Request error: %s", e)
53 |
54 | return photo_res.content
55 |
56 | def download_interesting_photos(self):
57 | photos_list_res = self.get_photo_list()
58 |
59 | photo_list = photos_list_res["photos"]["photo"]
60 |
61 | with ThreadPoolExecutor() as e:
62 | future_photos = {e.submit(self.download_photo, photo): photo for photo in photo_list}
63 |
64 | for future in as_completed(future_photos):
65 | photo_content = future.result()
66 | photo = future_photos[future]
67 | self.__save_photo(os.path.join(self.__download_location, photo["id"] + ".jpg"), photo_content)
68 |
69 | @staticmethod
70 | def __save_photo(filename, photo_content):
71 | try:
72 | with open(filename, "wb") as photo_file:
73 | photo_file.write(photo_content)
74 | except IOError as e:
75 | raise FlickrDownloaderException("Failed to save photo: %s", e)
76 |
--------------------------------------------------------------------------------
/section-3/bdd/src/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 |
4 | with open("README.md", "r") as readme_file:
5 | long_description = readme_file.read()
6 |
7 | setup(
8 | name="flickr_downloader",
9 | version="1.0.0-dev2",
10 | description="Python package that downloads 100 interesting photos from Flickr",
11 | long_description=long_description,
12 | long_description_content_type="text/markdown",
13 | author="Mihai Costea",
14 | license="MIT",
15 | classifiers=[
16 | "Development Status :: 4 - Beta",
17 | "Intended Audience :: Developers",
18 | "License :: OSI Approved :: MIT License"
19 | ],
20 | keywords="flickr photo downloader",
21 | packages=find_packages(),
22 | install_requires=["requests>=2"],
23 | python_requires="~=3.5"
24 | )
25 |
--------------------------------------------------------------------------------
/section-3/bdd/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-3/bdd/tests/__init__.py
--------------------------------------------------------------------------------
/section-3/bdd/tests/bdd/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-3/bdd/tests/bdd/__init__.py
--------------------------------------------------------------------------------
/section-3/bdd/tests/bdd/download_interesting_photos.feature:
--------------------------------------------------------------------------------
1 | Feature: Download 100 interesting photos from Flickr
2 |
3 |
4 | Scenario: Flickr sends out 100 interesting photos
5 | Given a valid Flickr API key and a valid download directory
6 | When we request to download 100 interesting photos from Flickr
7 | Then the download directory should contain 100 files
8 | And all the file names in the download directory should end with ".jpg"
9 |
--------------------------------------------------------------------------------
/section-3/bdd/tests/bdd/get_photo_list.feature:
--------------------------------------------------------------------------------
1 | Feature: Get a list of the most interesting photos from Flickr
2 |
3 | Scenario: Flickr returns a correct list of photos
4 | Given a valid Flickr API key
5 | When we request a list of photos from Flickr
6 | Then we should receive a response of dict type
7 | And there should be a "photos" field in the response
8 | And there should be a "photo" field in response["photos"]
9 | And there should be a list of dicts in response["photos"]["photo"]
10 | And each dict in the list should contain the fields "id", "farm", "server" and "secret"
11 |
--------------------------------------------------------------------------------
/section-3/bdd/tests/bdd/test_download_interesting_photos.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from pytest_bdd import scenario, given, when, then
4 | from src.flickr_downloader import FlickrDownloader
5 |
6 |
7 | @scenario('download_interesting_photos.feature', 'Flickr sends out 100 interesting photos')
8 | def test_download_interesting_photos():
9 | pass
10 |
11 |
12 | @given('a valid Flickr API key and a valid download directory')
13 | def valid_api_key(tmpdir):
14 | return {"downloader": FlickrDownloader("{}".format(os.environ["FLICKR_API_KEY"]), str(tmpdir)),
15 | "tmpdir": tmpdir}
16 |
17 |
18 | @when('we request to download 100 interesting photos from Flickr')
19 | def download_photos(valid_api_key):
20 | downloader = valid_api_key["downloader"]
21 | downloader.download_interesting_photos()
22 |
23 |
24 | @then('the download directory should contain 100 files')
25 | def receive_response(valid_api_key):
26 | tmpdir = valid_api_key["tmpdir"]
27 | assert len(os.listdir(tmpdir)) == 100
28 |
29 |
30 | @then('all the file names in the download directory should end with ".jpg"')
31 | def photos_in_response(valid_api_key):
32 | tmpdir = valid_api_key["tmpdir"]
33 | for file in os.listdir(tmpdir):
34 | filename = os.fsdecode(file)
35 | assert filename.endswith(".jpg")
36 |
--------------------------------------------------------------------------------
/section-3/bdd/tests/bdd/test_get_photo_list.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from pytest_bdd import scenario, given, when, then
4 | from src.flickr_downloader import FlickrDownloader
5 |
6 |
7 | @scenario('get_photo_list.feature', 'Flickr returns a correct list of photos')
8 | def test_get_photo_list():
9 | pass
10 |
11 |
12 | @given("a valid Flickr API key")
13 | def valid_api_key(tmpdir):
14 | return {"downloader": FlickrDownloader("{}".format(os.environ["FLICKR_API_KEY"]), str(tmpdir))}
15 |
16 |
17 | @when("we request a list of photos from Flickr")
18 | def request_photos(valid_api_key):
19 | valid_api_key["response"] = valid_api_key["downloader"].get_photo_list()
20 |
21 |
22 | @then("we should receive a response of dict type")
23 | def receive_response(valid_api_key):
24 | res = valid_api_key["response"]
25 | assert type(res) is dict
26 |
27 |
28 | @then('there should be a "photos" field in the response')
29 | def photos_in_response(valid_api_key):
30 | res = valid_api_key["response"]
31 | assert "photos" in res
32 |
33 |
34 | @then('there should be a "photo" field in response["photos"]')
35 | def photo_in_response_photos(valid_api_key):
36 | res = valid_api_key["response"]
37 | assert "photo" in res["photos"]
38 |
39 |
40 | @then('there should be a list of dicts in response["photos"]["photo"]')
41 | def list_in_response_photos_photo(valid_api_key):
42 | res = valid_api_key["response"]
43 | photo_list = res["photos"]["photo"]
44 | assert type(photo_list) is list
45 | for photo in photo_list:
46 | assert type(photo) is dict
47 |
48 |
49 | @then('each dict in the list should contain the fields "id", "farm", "server" and "secret"')
50 | def photo_files_in_response(valid_api_key):
51 | res = valid_api_key["response"]
52 | photo_list = res["photos"]["photo"]
53 |
54 | for photo in photo_list:
55 | assert "id" in photo
56 | assert "farm" in photo
57 | assert "server" in photo
58 | assert "secret" in photo
59 |
--------------------------------------------------------------------------------
/section-3/bdd/tests/integration/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-3/bdd/tests/integration/__init__.py
--------------------------------------------------------------------------------
/section-3/bdd/tests/integration/test_flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flickr_downloader import FlickrDownloader
4 |
5 |
6 | class TestFlickrDownloaderIntegration(object):
7 | def test_get_photo_list(self, tmpdir):
8 | downloader = FlickrDownloader("{}".format(os.environ["FLICKR_API_KEY"]), str(tmpdir))
9 |
10 | photo_list = downloader.get_photo_list()
11 |
12 | assert "photos" in photo_list
13 | assert "photo" in photo_list["photos"]
14 |
15 | for photo in photo_list["photos"]["photo"]:
16 | assert "id" in photo
17 | assert "farm" in photo
18 | assert "server" in photo
19 | assert "secret" in photo
20 |
21 | def test_download_interesting_photos(self, tmpdir):
22 | downloader = FlickrDownloader("{}".format(os.environ["FLICKR_API_KEY"]), str(tmpdir))
23 |
24 | downloader.download_interesting_photos()
25 |
26 | assert len(os.listdir(tmpdir)) == 100
27 |
28 | for file in os.listdir(tmpdir):
29 | filename = os.fsdecode(file)
30 | assert filename.endswith(".jpg")
31 |
--------------------------------------------------------------------------------
/section-3/bdd/tests/test_flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 | import requests
5 | from requests import RequestException
6 |
7 | from flickr_downloader import FlickrDownloader, FlickrDownloaderException
8 |
9 |
10 | class TestFlickrDownloader(object):
11 | def test_init_correct(self, tmpdir):
12 | try:
13 | FlickrDownloader("testkey", str(tmpdir))
14 | except FlickrDownloaderException as e:
15 | pytest.fail(e)
16 |
17 | def test_init_missing_download_folder(self):
18 | with pytest.raises(FlickrDownloaderException):
19 | FlickrDownloader("testkey", "amissingfolder")
20 |
21 | def test_init_download_folder_not_a_folder(self, tmpdir):
22 | f = tmpdir.join("afile.txt")
23 | f.write("test text")
24 |
25 | with pytest.raises(FlickrDownloaderException):
26 | FlickrDownloader("testkey", str(f))
27 |
28 | def test_init_api_key_none(self, tmpdir):
29 | with pytest.raises(FlickrDownloaderException):
30 | FlickrDownloader(None, str(tmpdir))
31 |
32 | def test_init_api_key_empty(self, tmpdir):
33 | with pytest.raises(FlickrDownloaderException):
34 | FlickrDownloader("", str(tmpdir))
35 |
36 | def test_get_photo_list_correct_url(self, tmpdir, mocker):
37 | response_mock = mocker.MagicMock()
38 | response_mock.json = mocker.MagicMock(return_value="valid json here")
39 |
40 | mocker.patch.object(requests, "get", return_value=response_mock)
41 |
42 | downloader = FlickrDownloader("test_key", str(tmpdir))
43 |
44 | photo_list = downloader.get_photo_list()
45 |
46 | assert photo_list == "valid json here"
47 |
48 | url = "https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=%s&format=json&nojsoncallback=1" % "test_key"
49 |
50 | requests.get.assert_called_once_with(url)
51 |
52 | def test_get_photo_list_failed_request(self, tmpdir, mocker):
53 | mocker.patch.object(requests, "get", side_effect=RequestException("test exception"))
54 |
55 | downloader = FlickrDownloader("test_key", str(tmpdir))
56 |
57 | with pytest.raises(FlickrDownloaderException):
58 | downloader.get_photo_list()
59 |
60 | def test_download_photo_correct(self, tmpdir, mocker):
61 | response_mock = mocker.MagicMock()
62 | response_mock.content = "test photo content"
63 |
64 | mocker.patch.object(requests, "get", return_value=response_mock)
65 |
66 | downloader = FlickrDownloader("test_key", str(tmpdir))
67 |
68 | photo = {
69 | "farm": "testfarm",
70 | "server": "testserver",
71 | "id": "testid",
72 | "secret": "testsecret"
73 | }
74 |
75 | photo_content = downloader.download_photo(photo.copy())
76 |
77 | assert photo_content == "test photo content"
78 | photo_url = "https://farm%s.staticflickr.com/%s/%s_%s.jpg" % (photo["farm"], photo["server"], photo["id"], photo["secret"])
79 | requests.get.assert_called_once_with(photo_url)
80 |
81 | def test_download_photo_missing_keys(self, tmpdir, mocker):
82 | response_mock = mocker.MagicMock()
83 | response_mock.content = "test photo content"
84 |
85 | mocker.patch.object(requests, "get", return_value=response_mock)
86 |
87 | downloader = FlickrDownloader("test_key", str(tmpdir))
88 |
89 | photo = {
90 | "server": "testserver",
91 | "id": "testid",
92 | "secret": "testsecret"
93 | }
94 |
95 | with pytest.raises(FlickrDownloaderException):
96 | downloader.download_photo(photo)
97 |
98 | photo = {
99 | "farm": "testfarm",
100 | "id": "testid",
101 | "secret": "testsecret"
102 | }
103 |
104 | with pytest.raises(FlickrDownloaderException):
105 | downloader.download_photo(photo)
106 |
107 | photo = {
108 | "farm": "testfarm",
109 | "server": "testserver",
110 | "secret": "testsecret"
111 | }
112 |
113 | with pytest.raises(FlickrDownloaderException):
114 | downloader.download_photo(photo)
115 |
116 | photo = {
117 | "farm": "testfarm",
118 | "server": "testserver",
119 | "id": "testid"
120 | }
121 |
122 | with pytest.raises(FlickrDownloaderException):
123 | downloader.download_photo(photo)
124 |
125 | def test_download_photo_failed_request(self, tmpdir, mocker):
126 | mocker.patch.object(requests, "get", side_effect=RequestException("test exception"))
127 | downloader = FlickrDownloader("test_key", str(tmpdir))
128 |
129 | photo = {
130 | "farm": "testfarm",
131 | "server": "testserver",
132 | "id": "testid",
133 | "secret": "testsecret"
134 | }
135 |
136 | with pytest.raises(FlickrDownloaderException):
137 | downloader.download_photo(photo)
138 |
139 | def test_download_interesting_photos_correct(self, tmpdir, mocker):
140 | photos_list_mock = {
141 | "photos": {
142 | "photo": [
143 | {
144 | "id": "1"
145 | },
146 | {
147 | "id": "2"
148 | },
149 | {
150 | "id": "3"
151 | }
152 | ]
153 | }
154 | }
155 |
156 | # Mock the downloader class
157 | get_photo_list_mock = mocker.patch.object(FlickrDownloader, "get_photo_list")
158 | download_photo_mock = mocker.patch.object(FlickrDownloader, "download_photo")
159 | save_photo_mock = mocker.patch.object(FlickrDownloader, "_FlickrDownloader__save_photo")
160 |
161 | get_photo_list_mock.return_value = photos_list_mock
162 | download_photo_mock.return_value = "photo content"
163 |
164 | downloader = FlickrDownloader("test_key", str(tmpdir))
165 |
166 | downloader.download_interesting_photos()
167 |
168 | get_photo_list_mock.assert_called_once()
169 | download_photo_mock.assert_any_call({"id": "1"})
170 | download_photo_mock.assert_any_call({"id": "2"})
171 | download_photo_mock.assert_any_call({"id": "3"})
172 | save_photo_mock.assert_any_call(os.path.join(str(tmpdir), "1.jpg"), "photo content")
173 | save_photo_mock.assert_any_call(os.path.join(str(tmpdir), "2.jpg"), "photo content")
174 | save_photo_mock.assert_any_call(os.path.join(str(tmpdir), "3.jpg"), "photo content")
175 |
--------------------------------------------------------------------------------
/section-3/integration/requirements.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | pytest-mock
3 | coverage
4 |
--------------------------------------------------------------------------------
/section-3/integration/src/flickr_downloader/__init__.py:
--------------------------------------------------------------------------------
1 | from .flickr_downloader import FlickrDownloader, FlickrDownloaderException
2 |
--------------------------------------------------------------------------------
/section-3/integration/src/flickr_downloader/flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import os
3 | import logging
4 | from concurrent.futures import ThreadPoolExecutor, as_completed
5 | from requests import RequestException
6 |
7 | logger = logging.getLogger(__name__)
8 | logger.setLevel(logging.DEBUG)
9 |
10 |
11 | class FlickrDownloaderException(Exception):
12 | pass
13 |
14 |
15 | class FlickrDownloader:
16 | def __init__(self, api_key, download_location):
17 | if not isinstance(api_key, str):
18 | raise FlickrDownloaderException("api_key should be str")
19 |
20 | if not isinstance(download_location, str):
21 | raise FlickrDownloaderException("download_location should be str")
22 |
23 | if api_key is None or len(api_key) == 0:
24 | raise FlickrDownloaderException("API Key is not set")
25 |
26 | if not os.path.exists(download_location) or not os.path.isdir(download_location):
27 | raise FlickrDownloaderException("Download location does not exist or is not a directory")
28 |
29 | self.__api_key = api_key
30 | self.__download_location = download_location
31 |
32 | def get_photo_list(self):
33 | url = "https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=%s&format=json&nojsoncallback=1" % self.__api_key
34 |
35 | try:
36 | res = requests.get(url).json()
37 | except RequestException as e:
38 | raise FlickrDownloaderException("Request error: %s", e)
39 |
40 | return res
41 |
42 | @staticmethod
43 | def download_photo(photo):
44 | try:
45 | photo_url = "https://farm%s.staticflickr.com/%s/%s_%s.jpg" % (photo["farm"], photo["server"], photo["id"], photo["secret"])
46 | except KeyError as e:
47 | raise FlickrDownloaderException("photo argument is invalid: %s" % e)
48 |
49 | try:
50 | photo_res = requests.get(photo_url)
51 | except RequestException as e:
52 | raise FlickrDownloaderException("Request error: %s", e)
53 |
54 | return photo_res.content
55 |
56 | def download_interesting_photos(self):
57 | photos_list_res = self.get_photo_list()
58 |
59 | photo_list = photos_list_res["photos"]["photo"]
60 |
61 | with ThreadPoolExecutor() as e:
62 | future_photos = {e.submit(self.download_photo, photo): photo for photo in photo_list}
63 |
64 | for future in as_completed(future_photos):
65 | photo_content = future.result()
66 | photo = future_photos[future]
67 | self.__save_photo(os.path.join(self.__download_location, photo["id"] + ".jpg"), photo_content)
68 |
69 | @staticmethod
70 | def __save_photo(filename, photo_content):
71 | try:
72 | with open(filename, "wb") as photo_file:
73 | photo_file.write(photo_content)
74 | except IOError as e:
75 | raise FlickrDownloaderException("Failed to save photo: %s", e)
76 |
--------------------------------------------------------------------------------
/section-3/integration/src/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 |
4 | with open("README.md", "r") as readme_file:
5 | long_description = readme_file.read()
6 |
7 | setup(
8 | name="flickr_downloader",
9 | version="1.0.0-dev2",
10 | description="Python package that downloads 100 interesting photos from Flickr",
11 | long_description=long_description,
12 | long_description_content_type="text/markdown",
13 | author="Mihai Costea",
14 | license="MIT",
15 | classifiers=[
16 | "Development Status :: 4 - Beta",
17 | "Intended Audience :: Developers",
18 | "License :: OSI Approved :: MIT License"
19 | ],
20 | keywords="flickr photo downloader",
21 | packages=find_packages(),
22 | install_requires=["requests>=2"],
23 | python_requires="~=3.5"
24 | )
25 |
--------------------------------------------------------------------------------
/section-3/integration/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-3/integration/tests/__init__.py
--------------------------------------------------------------------------------
/section-3/integration/tests/integration/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-3/integration/tests/integration/__init__.py
--------------------------------------------------------------------------------
/section-3/integration/tests/integration/test_flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flickr_downloader import FlickrDownloader
4 |
5 |
6 | class TestFlickrDownloaderIntegration(object):
7 | def test_get_get_photo_list(self, tmpdir):
8 | downloader = FlickrDownloader("{}".format(os.environ["FLICKR_API_KEY"]), str(tmpdir))
9 |
10 | photo_list = downloader.get_photo_list()
11 |
12 | assert "photos" in photo_list
13 | assert "photo" in photo_list["photos"]
14 |
15 | for photo in photo_list["photos"]["photo"]:
16 | assert "id" in photo
17 | assert "farm" in photo
18 | assert "server" in photo
19 | assert "id" in photo
20 | assert "secret" in photo
21 |
22 | def test_download_interesting_photos(self, tmpdir):
23 | downloader = FlickrDownloader("{}".format(os.environ["FLICKR_API_KEY"]), str(tmpdir))
24 |
25 | downloader.download_interesting_photos()
26 |
27 | assert len(os.listdir(tmpdir)) == 100
28 |
29 | for file in os.listdir(tmpdir):
30 | filename = os.fsdecode(file)
31 | assert filename.endswith(".jpg")
32 |
--------------------------------------------------------------------------------
/section-3/integration/tests/test_flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 | import requests
5 | from requests import RequestException
6 |
7 | from flickr_downloader import FlickrDownloader, FlickrDownloaderException
8 |
9 |
10 | class TestFlickrDownloader(object):
11 | def test_init_correct(self, tmpdir):
12 | try:
13 | FlickrDownloader("testkey", str(tmpdir))
14 | except FlickrDownloaderException as e:
15 | pytest.fail(e)
16 |
17 | def test_init_missing_download_folder(self):
18 | with pytest.raises(FlickrDownloaderException):
19 | FlickrDownloader("testkey", "amissingfolder")
20 |
21 | def test_init_download_folder_not_a_folder(self, tmpdir):
22 | f = tmpdir.join("afile.txt")
23 | f.write("test text")
24 |
25 | with pytest.raises(FlickrDownloaderException):
26 | FlickrDownloader("testkey", str(f))
27 |
28 | def test_init_api_key_none(self, tmpdir):
29 | with pytest.raises(FlickrDownloaderException):
30 | FlickrDownloader(None, str(tmpdir))
31 |
32 | def test_init_api_key_empty(self, tmpdir):
33 | with pytest.raises(FlickrDownloaderException):
34 | FlickrDownloader("", str(tmpdir))
35 |
36 | def test_get_photo_list_correct_url(self, tmpdir, mocker):
37 | response_mock = mocker.MagicMock()
38 | response_mock.json = mocker.MagicMock(return_value="valid json here")
39 |
40 | mocker.patch.object(requests, "get", return_value=response_mock)
41 |
42 | downloader = FlickrDownloader("test_key", str(tmpdir))
43 |
44 | photo_list = downloader.get_photo_list()
45 |
46 | assert photo_list == "valid json here"
47 |
48 | url = "https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=%s&format=json&nojsoncallback=1" % "test_key"
49 |
50 | requests.get.assert_called_once_with(url)
51 |
52 | def test_get_photo_list_failed_request(self, tmpdir, mocker):
53 | mocker.patch.object(requests, "get", side_effect=RequestException("test exception"))
54 |
55 | downloader = FlickrDownloader("test_key", str(tmpdir))
56 |
57 | with pytest.raises(FlickrDownloaderException):
58 | downloader.get_photo_list()
59 |
60 | def test_download_photo_correct(self, tmpdir, mocker):
61 | response_mock = mocker.MagicMock()
62 | response_mock.content = "test photo content"
63 |
64 | mocker.patch.object(requests, "get", return_value=response_mock)
65 |
66 | downloader = FlickrDownloader("test_key", str(tmpdir))
67 |
68 | photo = {
69 | "farm": "testfarm",
70 | "server": "testserver",
71 | "id": "testid",
72 | "secret": "testsecret"
73 | }
74 |
75 | photo_content = downloader.download_photo(photo.copy())
76 |
77 | assert photo_content == "test photo content"
78 | photo_url = "https://farm%s.staticflickr.com/%s/%s_%s.jpg" % (photo["farm"], photo["server"], photo["id"], photo["secret"])
79 | requests.get.assert_called_once_with(photo_url)
80 |
81 | def test_download_photo_missing_keys(self, tmpdir, mocker):
82 | response_mock = mocker.MagicMock()
83 | response_mock.content = "test photo content"
84 |
85 | mocker.patch.object(requests, "get", return_value=response_mock)
86 |
87 | downloader = FlickrDownloader("test_key", str(tmpdir))
88 |
89 | photo = {
90 | "server": "testserver",
91 | "id": "testid",
92 | "secret": "testsecret"
93 | }
94 |
95 | with pytest.raises(FlickrDownloaderException):
96 | downloader.download_photo(photo)
97 |
98 | photo = {
99 | "farm": "testfarm",
100 | "id": "testid",
101 | "secret": "testsecret"
102 | }
103 |
104 | with pytest.raises(FlickrDownloaderException):
105 | downloader.download_photo(photo)
106 |
107 | photo = {
108 | "farm": "testfarm",
109 | "server": "testserver",
110 | "secret": "testsecret"
111 | }
112 |
113 | with pytest.raises(FlickrDownloaderException):
114 | downloader.download_photo(photo)
115 |
116 | photo = {
117 | "farm": "testfarm",
118 | "server": "testserver",
119 | "id": "testid"
120 | }
121 |
122 | with pytest.raises(FlickrDownloaderException):
123 | downloader.download_photo(photo)
124 |
125 | def test_download_photo_failed_request(self, tmpdir, mocker):
126 | mocker.patch.object(requests, "get", side_effect=RequestException("test exception"))
127 | downloader = FlickrDownloader("test_key", str(tmpdir))
128 |
129 | photo = {
130 | "farm": "testfarm",
131 | "server": "testserver",
132 | "id": "testid",
133 | "secret": "testsecret"
134 | }
135 |
136 | with pytest.raises(FlickrDownloaderException):
137 | downloader.download_photo(photo)
138 |
139 | def test_download_interesting_photos_correct(self, tmpdir, mocker):
140 | photos_list_mock = {
141 | "photos": {
142 | "photo": [
143 | {
144 | "id": "1"
145 | },
146 | {
147 | "id": "2"
148 | },
149 | {
150 | "id": "3"
151 | }
152 | ]
153 | }
154 | }
155 |
156 | # Mock the downloader class
157 | get_photo_list_mock = mocker.patch.object(FlickrDownloader, "get_photo_list")
158 | download_photo_mock = mocker.patch.object(FlickrDownloader, "download_photo")
159 | save_photo_mock = mocker.patch.object(FlickrDownloader, "_FlickrDownloader__save_photo")
160 |
161 | get_photo_list_mock.return_value = photos_list_mock
162 | download_photo_mock.return_value = "photo content"
163 |
164 | downloader = FlickrDownloader("test_key", str(tmpdir))
165 |
166 | downloader.download_interesting_photos()
167 |
168 | get_photo_list_mock.assert_called_once()
169 | download_photo_mock.assert_any_call({"id": "1"})
170 | download_photo_mock.assert_any_call({"id": "2"})
171 | download_photo_mock.assert_any_call({"id": "3"})
172 | save_photo_mock.assert_any_call(os.path.join(str(tmpdir), "1.jpg"), "photo content")
173 | save_photo_mock.assert_any_call(os.path.join(str(tmpdir), "2.jpg"), "photo content")
174 | save_photo_mock.assert_any_call(os.path.join(str(tmpdir), "3.jpg"), "photo content")
175 |
--------------------------------------------------------------------------------
/section-3/tdd/flickr_downloader/__init__.py:
--------------------------------------------------------------------------------
1 | from .flickr_downloader import FlickrDownloader, FlickrDownloaderException
2 |
--------------------------------------------------------------------------------
/section-3/tdd/flickr_downloader/flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import os
3 | import logging
4 | from concurrent.futures import ThreadPoolExecutor, as_completed
5 | from requests import RequestException
6 |
7 | logger = logging.getLogger(__name__)
8 | logger.setLevel(logging.DEBUG)
9 |
10 |
11 | class FlickrDownloaderException(Exception):
12 | pass
13 |
14 |
15 | class FlickrDownloader:
16 | def __init__(self, api_key, download_location):
17 | if not isinstance(api_key, str):
18 | raise FlickrDownloaderException("api_key should be str")
19 |
20 | if not isinstance(download_location, str):
21 | raise FlickrDownloaderException("download_location should be str")
22 |
23 | if api_key is None or len(api_key) == 0:
24 | raise FlickrDownloaderException("API Key is not set")
25 |
26 | if not os.path.exists(download_location) or not os.path.isdir(download_location):
27 | raise FlickrDownloaderException("Download location does not exist or is not a directory")
28 |
29 | self.__api_key = api_key
30 | self.__download_location = download_location
31 |
32 | def get_photo_list(self):
33 | url = "https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=%s&format=json&nojsoncallback=1" % self.__api_key
34 |
35 | try:
36 | res = requests.get(url).json()
37 | except RequestException as e:
38 | raise FlickrDownloaderException("Request error: %s", e)
39 |
40 | return res
41 |
42 | @staticmethod
43 | def download_photo(photo):
44 | try:
45 | photo_url = "https://farm%s.staticflickr.com/%s/%s_%s.jpg" % (photo["farm"], photo["server"], photo["id"], photo["secret"])
46 | except KeyError as e:
47 | raise FlickrDownloaderException("photo argument is invalid: %s" % e)
48 |
49 | try:
50 | photo_res = requests.get(photo_url)
51 | except RequestException as e:
52 | raise FlickrDownloaderException("Request error: %s", e)
53 |
54 | return photo_res.content
55 |
56 | def download_interesting_photos(self):
57 | photos_list_res = self.get_photo_list()
58 |
59 | photo_list = photos_list_res["photos"]["photo"]
60 |
61 | with ThreadPoolExecutor() as e:
62 | future_photos = {e.submit(self.download_photo, photo): photo for photo in photo_list}
63 |
64 | for future in as_completed(future_photos):
65 | photo_content = future.result()
66 | photo = future_photos[future]
67 | self.save_photo(os.path.join(self.__download_location, photo["id"] + ".jpg"), photo_content)
68 |
69 | @staticmethod
70 | def save_photo(filename, photo_content):
71 | try:
72 | with open(filename, "wb") as photo_file:
73 | photo_file.write(photo_content)
74 | except IOError as e:
75 | raise FlickrDownloaderException("Failed to save photo: %s", e)
76 |
--------------------------------------------------------------------------------
/section-3/tdd/requirements.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | pytest-mock
3 | requests
--------------------------------------------------------------------------------
/section-3/tdd/specific_flickr_downloader/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-3/tdd/specific_flickr_downloader/__init__.py
--------------------------------------------------------------------------------
/section-3/tdd/specific_flickr_downloader/specific_flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import os
2 | from flickr_downloader.flickr_downloader import FlickrDownloader
3 |
4 |
5 | def download_specific_photo(photo_id, download_folder, flickr_api_key):
6 | downloader = FlickrDownloader(flickr_api_key, download_folder)
7 |
8 | photo_list_result = downloader.get_photo_list()
9 | photo_list = photo_list_result["photos"]["photo"]
10 |
11 | requested_photo = [photo for photo in photo_list if photo["id"] == photo_id]
12 |
13 | photo_location = os.path.join(download_folder, "{}.jpg".format(photo_id))
14 |
15 | photo_content = downloader.download_photo(requested_photo)
16 |
17 | downloader.save_photo(photo_location, photo_content)
18 |
19 | return photo_location
20 |
--------------------------------------------------------------------------------
/section-3/tdd/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-3/tdd/tests/__init__.py
--------------------------------------------------------------------------------
/section-3/tdd/tests/test_specific_flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import os
3 | from specific_flickr_downloader.specific_flickr_downloader import download_specific_photo
4 | from specific_flickr_downloader.specific_flickr_downloader import FlickrDownloader
5 |
6 |
7 | class TestSpecificFlickrDownloader(object):
8 | def test_download_specific_photo(self, tmpdir, mocker):
9 | photo_id = "2"
10 | download_location = str(tmpdir)
11 | expected_return = os.path.join(tmpdir, "{}.jpg".format(photo_id))
12 | flickr_api_key = "testkey"
13 |
14 | flickr_downloader_init_mock = mocker.patch.object(FlickrDownloader, "__init__")
15 | get_photo_list_mock = mocker.patch.object(FlickrDownloader, "get_photo_list")
16 | download_photo_mock = mocker.patch.object(FlickrDownloader, "download_photo")
17 | save_photo_mock = mocker.patch.object(FlickrDownloader, "save_photo")
18 |
19 | flickr_downloader_init_mock.return_value = None
20 |
21 | photos_list_mock = {
22 | "photos": {
23 | "photo": [
24 | {
25 | "id": "1"
26 | },
27 | {
28 | "id": "2"
29 | },
30 | {
31 | "id": "3"
32 | }
33 | ]
34 | }
35 | }
36 |
37 | get_photo_list_mock.return_value = photos_list_mock
38 | download_photo_mock.return_value = "photo content"
39 |
40 | returned_value = download_specific_photo(photo_id, download_location, flickr_api_key)
41 |
42 | assert expected_return == returned_value
43 | save_photo_mock.assert_called_once_with(expected_return, "photo content")
44 |
--------------------------------------------------------------------------------
/section-3/unit/requirements.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | pytest-mock
3 | coverage
4 |
--------------------------------------------------------------------------------
/section-3/unit/src/flickr_downloader/__init__.py:
--------------------------------------------------------------------------------
1 | from .flickr_downloader import FlickrDownloader, FlickrDownloaderException
2 |
--------------------------------------------------------------------------------
/section-3/unit/src/flickr_downloader/flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import os
3 | import logging
4 | from concurrent.futures import ThreadPoolExecutor, as_completed
5 | from requests import RequestException
6 |
7 | logger = logging.getLogger(__name__)
8 | logger.setLevel(logging.DEBUG)
9 |
10 |
11 | class FlickrDownloaderException(Exception):
12 | pass
13 |
14 |
15 | class FlickrDownloader:
16 | def __init__(self, api_key, download_location):
17 | if not isinstance(api_key, str):
18 | raise FlickrDownloaderException("api_key should be str")
19 |
20 | if not isinstance(download_location, str):
21 | raise FlickrDownloaderException("download_location should be str")
22 |
23 | if api_key is None or len(api_key) == 0:
24 | raise FlickrDownloaderException("API Key is not set")
25 |
26 | if not os.path.exists(download_location) or not os.path.isdir(download_location):
27 | raise FlickrDownloaderException("Download location does not exist or is not a directory")
28 |
29 | self.__api_key = api_key
30 | self.__download_location = download_location
31 |
32 | def get_photo_list(self):
33 | url = "https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=%s&format=json&nojsoncallback=1" % self.__api_key
34 |
35 | try:
36 | res = requests.get(url).json()
37 | except RequestException as e:
38 | raise FlickrDownloaderException("Request error: %s", e)
39 |
40 | return res
41 |
42 | @staticmethod
43 | def download_photo(photo):
44 | try:
45 | photo_url = "https://farm%s.staticflickr.com/%s/%s_%s.jpg" % (photo["farm"], photo["server"], photo["id"], photo["secret"])
46 | except KeyError as e:
47 | raise FlickrDownloaderException("photo argument is invalid: %s" % e)
48 |
49 | try:
50 | photo_res = requests.get(photo_url)
51 | except RequestException as e:
52 | raise FlickrDownloaderException("Request error: %s", e)
53 |
54 | return photo_res.content
55 |
56 | def download_interesting_photos(self):
57 | photos_list_res = self.get_photo_list()
58 |
59 | photo_list = photos_list_res["photos"]["photo"]
60 |
61 | with ThreadPoolExecutor() as e:
62 | future_photos = {e.submit(self.download_photo, photo): photo for photo in photo_list}
63 |
64 | for future in as_completed(future_photos):
65 | photo_content = future.result()
66 | photo = future_photos[future]
67 | self.save_photo(os.path.join(self.__download_location, photo["id"] + ".jpg"), photo_content)
68 |
69 | @staticmethod
70 | def save_photo(filename, photo_content):
71 | try:
72 | with open(filename, "wb") as photo_file:
73 | photo_file.write(photo_content)
74 | except IOError as e:
75 | raise FlickrDownloaderException("Failed to save photo: %s", e)
76 |
--------------------------------------------------------------------------------
/section-3/unit/src/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 |
4 | with open("README.md", "r") as readme_file:
5 | long_description = readme_file.read()
6 |
7 | setup(
8 | name="flickr_downloader",
9 | version="1.0.0-dev2",
10 | description="Python package that downloads 100 interesting photos from Flickr",
11 | long_description=long_description,
12 | long_description_content_type="text/markdown",
13 | author="Mihai Costea",
14 | license="MIT",
15 | classifiers=[
16 | "Development Status :: 4 - Beta",
17 | "Intended Audience :: Developers",
18 | "License :: OSI Approved :: MIT License"
19 | ],
20 | keywords="flickr photo downloader",
21 | packages=find_packages(),
22 | install_requires=["requests>=2"],
23 | python_requires="~=3.5"
24 | )
25 |
--------------------------------------------------------------------------------
/section-3/unit/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-3/unit/tests/__init__.py
--------------------------------------------------------------------------------
/section-3/unit/tests/test_flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 | import requests
5 | from requests import RequestException
6 |
7 | from flickr_downloader import FlickrDownloader, FlickrDownloaderException
8 |
9 |
10 | class TestFlickrDownloader(object):
11 | def test_init_correct(self, tmpdir):
12 | try:
13 | FlickrDownloader("testkey", str(tmpdir))
14 | except FlickrDownloaderException as e:
15 | pytest.fail(e)
16 |
17 | def test_init_missing_download_folder(self):
18 | with pytest.raises(FlickrDownloaderException):
19 | FlickrDownloader("testkey", "amissingfolder")
20 |
21 | def test_init_download_folder_not_a_folder(self, tmpdir):
22 | f = tmpdir.join("afile.txt")
23 | f.write("test text")
24 |
25 | with pytest.raises(FlickrDownloaderException):
26 | FlickrDownloader("testkey", str(f))
27 |
28 | def test_init_api_key_none(self, tmpdir):
29 | with pytest.raises(FlickrDownloaderException):
30 | FlickrDownloader(None, str(tmpdir))
31 |
32 | def test_init_api_key_empty(self, tmpdir):
33 | with pytest.raises(FlickrDownloaderException):
34 | FlickrDownloader("", str(tmpdir))
35 |
36 | def test_get_photo_list_correct_url(self, tmpdir, mocker):
37 | response_mock = mocker.MagicMock()
38 | response_mock.json = mocker.MagicMock(return_value="valid json here")
39 |
40 | mocker.patch.object(requests, "get", return_value=response_mock)
41 |
42 | downloader = FlickrDownloader("test_key", str(tmpdir))
43 |
44 | photo_list = downloader.get_photo_list()
45 |
46 | assert photo_list == "valid json here"
47 |
48 | url = "https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=%s&format=json&nojsoncallback=1" % "test_key"
49 |
50 | requests.get.assert_called_once_with(url)
51 |
52 | def test_get_photo_list_failed_request(self, tmpdir, mocker):
53 | mocker.patch.object(requests, "get", side_effect=RequestException("test exception"))
54 |
55 | downloader = FlickrDownloader("test_key", str(tmpdir))
56 |
57 | with pytest.raises(FlickrDownloaderException):
58 | downloader.get_photo_list()
59 |
60 | def test_download_photo_correct(self, tmpdir, mocker):
61 | response_mock = mocker.MagicMock()
62 | response_mock.content = "test photo content"
63 |
64 | mocker.patch.object(requests, "get", return_value=response_mock)
65 |
66 | downloader = FlickrDownloader("test_key", str(tmpdir))
67 |
68 | photo = {
69 | "farm": "testfarm",
70 | "server": "testserver",
71 | "id": "testid",
72 | "secret": "testsecret"
73 | }
74 |
75 | photo_content = downloader.download_photo(photo.copy())
76 |
77 | assert photo_content == "test photo content"
78 | photo_url = "https://farm%s.staticflickr.com/%s/%s_%s.jpg" % (photo["farm"], photo["server"], photo["id"], photo["secret"])
79 | requests.get.assert_called_once_with(photo_url)
80 |
81 | def test_download_photo_missing_keys(self, tmpdir, mocker):
82 | response_mock = mocker.MagicMock()
83 | response_mock.content = "test photo content"
84 |
85 | mocker.patch.object(requests, "get", return_value=response_mock)
86 |
87 | downloader = FlickrDownloader("test_key", str(tmpdir))
88 |
89 | photo = {
90 | "server": "testserver",
91 | "id": "testid",
92 | "secret": "testsecret"
93 | }
94 |
95 | with pytest.raises(FlickrDownloaderException):
96 | downloader.download_photo(photo)
97 |
98 | photo = {
99 | "farm": "testfarm",
100 | "id": "testid",
101 | "secret": "testsecret"
102 | }
103 |
104 | with pytest.raises(FlickrDownloaderException):
105 | downloader.download_photo(photo)
106 |
107 | photo = {
108 | "farm": "testfarm",
109 | "server": "testserver",
110 | "secret": "testsecret"
111 | }
112 |
113 | with pytest.raises(FlickrDownloaderException):
114 | downloader.download_photo(photo)
115 |
116 | photo = {
117 | "farm": "testfarm",
118 | "server": "testserver",
119 | "id": "testid"
120 | }
121 |
122 | with pytest.raises(FlickrDownloaderException):
123 | downloader.download_photo(photo)
124 |
125 | def test_download_photo_failed_request(self, tmpdir, mocker):
126 | mocker.patch.object(requests, "get", side_effect=RequestException("test exception"))
127 | downloader = FlickrDownloader("test_key", str(tmpdir))
128 |
129 | photo = {
130 | "farm": "testfarm",
131 | "server": "testserver",
132 | "id": "testid",
133 | "secret": "testsecret"
134 | }
135 |
136 | with pytest.raises(FlickrDownloaderException):
137 | downloader.download_photo(photo)
138 |
139 | def test_download_interesting_photos_correct(self, tmpdir, mocker):
140 | photos_list_mock = {
141 | "photos": {
142 | "photo": [
143 | {
144 | "id": "1"
145 | },
146 | {
147 | "id": "2"
148 | },
149 | {
150 | "id": "3"
151 | }
152 | ]
153 | }
154 | }
155 |
156 | # Mock the downloader class
157 | get_photo_list_mock = mocker.patch.object(FlickrDownloader, "get_photo_list")
158 | download_photo_mock = mocker.patch.object(FlickrDownloader, "download_photo")
159 | save_photo_mock = mocker.patch.object(FlickrDownloader, "save_photo")
160 |
161 | get_photo_list_mock.return_value = photos_list_mock
162 | download_photo_mock.return_value = "photo content"
163 |
164 | downloader = FlickrDownloader("test_key", str(tmpdir))
165 |
166 | downloader.download_interesting_photos()
167 |
168 | get_photo_list_mock.assert_called_once()
169 | download_photo_mock.assert_any_call({"id": "1"})
170 | download_photo_mock.assert_any_call({"id": "2"})
171 | download_photo_mock.assert_any_call({"id": "3"})
172 | save_photo_mock.assert_any_call(os.path.join(str(tmpdir), "1.jpg"), "photo content")
173 | save_photo_mock.assert_any_call(os.path.join(str(tmpdir), "2.jpg"), "photo content")
174 | save_photo_mock.assert_any_call(os.path.join(str(tmpdir), "3.jpg"), "photo content")
175 |
--------------------------------------------------------------------------------
/section-4/debugging/histogram/Nature-View.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-4/debugging/histogram/Nature-View.jpg
--------------------------------------------------------------------------------
/section-4/debugging/histogram/pypy.py:
--------------------------------------------------------------------------------
1 | import os
2 | from PIL import Image
3 |
4 |
5 | def calculate_histogram(photo_):
6 | bins = []
7 | for i in range(256):
8 | bins.append(0)
9 |
10 | width, height = photo_.size
11 |
12 | for i in range(width):
13 | for j in range(height):
14 | intensity = sum(photo.getpixel((i, j)))
15 | bins[int(intensity/3)] += 1
16 |
17 | return bins
18 |
19 |
20 | if __name__ == "__main__":
21 | # Photo source: https://commons.wikimedia.org/wiki/File:Nature-View.jpg
22 | photo = Image.open(os.path.join(os.path.realpath(os.path.dirname(__file__)), "Nature-View.jpg"))
23 | histogram = calculate_histogram(photo)
24 | print(histogram)
25 |
--------------------------------------------------------------------------------
/section-4/envs/.gitignore:
--------------------------------------------------------------------------------
1 | py3venv
2 | pypy3venv
3 |
--------------------------------------------------------------------------------
/section-4/envs/README.md:
--------------------------------------------------------------------------------
1 | # Create a new Python virtualenv
2 | ```
3 | $ python3 -m venv py3venv
4 | ```
5 |
6 | # Activate a Python virtualenv
7 | ```
8 | $ source py3venv/bin/activate
9 | ```
10 |
11 | # Specify a Python interpreter on virtualenv creation
12 | ```
13 | $ python3 -m venv -p pypy3 pypy3venv
14 | ```
15 |
16 |
17 |
--------------------------------------------------------------------------------
/section-4/envs/histogram/Nature-View.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-4/envs/histogram/Nature-View.jpg
--------------------------------------------------------------------------------
/section-4/envs/histogram/pypy.py:
--------------------------------------------------------------------------------
1 | import os
2 | from PIL import Image
3 |
4 |
5 | def calculate_histogram(photo_):
6 | bins = []
7 | for i in range(256):
8 | bins.append(0)
9 |
10 | width, height = photo_.size
11 |
12 | for i in range(width):
13 | for j in range(height):
14 | intensity = sum(photo.getpixel((i, j)))
15 | bins[int(intensity/3)] += 1
16 |
17 | return bins
18 |
19 |
20 | if __name__ == "__main__":
21 | # Photo source: https://commons.wikimedia.org/wiki/File:Nature-View.jpg
22 | photo = Image.open(os.path.join(os.path.realpath(os.path.dirname(__file__)), "Nature-View.jpg"))
23 | histogram = calculate_histogram(photo)
24 | print(histogram)
25 |
--------------------------------------------------------------------------------
/section-4/pip/main.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 |
3 | app = Flask(__name__)
4 |
5 |
6 | @app.route("/")
7 | def hello():
8 | return "Hello World!"
9 |
10 |
11 | if __name__ == "__main__":
12 | app.run(host="0.0.0.0")
13 |
14 |
--------------------------------------------------------------------------------
/section-4/pip/requirements.txt:
--------------------------------------------------------------------------------
1 | click==6.7
2 | Flask==1.0.2
3 | gunicorn==19.8.1
4 | itsdangerous==0.24
5 | Jinja2==2.10
6 | MarkupSafe==1.0
7 | pkg-resources==0.0.0
8 | Werkzeug==0.14.1
9 |
--------------------------------------------------------------------------------
/section-4/prototyping/requests_library.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Importing"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "There's just a single import for this module. Pretty easy."
15 | ]
16 | },
17 | {
18 | "cell_type": "code",
19 | "execution_count": 1,
20 | "metadata": {},
21 | "outputs": [],
22 | "source": [
23 | "import requests"
24 | ]
25 | },
26 | {
27 | "cell_type": "markdown",
28 | "metadata": {},
29 | "source": [
30 | "# Get request"
31 | ]
32 | },
33 | {
34 | "cell_type": "markdown",
35 | "metadata": {},
36 | "source": [
37 | "There's a method for each HTTP action. See get below."
38 | ]
39 | },
40 | {
41 | "cell_type": "code",
42 | "execution_count": 2,
43 | "metadata": {},
44 | "outputs": [],
45 | "source": [
46 | "r = requests.get(\"https://api.lyrics.ovh/v1/Coldplay/Adventure of a Lifetime\")"
47 | ]
48 | },
49 | {
50 | "cell_type": "markdown",
51 | "metadata": {},
52 | "source": [
53 | "You get back a response object"
54 | ]
55 | },
56 | {
57 | "cell_type": "code",
58 | "execution_count": 3,
59 | "metadata": {},
60 | "outputs": [
61 | {
62 | "data": {
63 | "text/plain": [
64 | "requests.models.Response"
65 | ]
66 | },
67 | "execution_count": 3,
68 | "metadata": {},
69 | "output_type": "execute_result"
70 | }
71 | ],
72 | "source": [
73 | "type(r)"
74 | ]
75 | },
76 | {
77 | "cell_type": "markdown",
78 | "metadata": {},
79 | "source": [
80 | "You can get the raw test response or a parsed json response directly."
81 | ]
82 | },
83 | {
84 | "cell_type": "code",
85 | "execution_count": 4,
86 | "metadata": {},
87 | "outputs": [
88 | {
89 | "data": {
90 | "text/plain": [
91 | "'{\"lyrics\":\"Turn your magic on, Umi she\\'d say\\\\r\\\\nEverything you want\\'s a dream away\\\\r\\\\nWe are legends, every day\\\\r\\\\n\\\\nThat\\'s what she told me\\\\r\\\\nTurn your magic on, to me she\\'d say\\\\n\\\\nEverything you want\\'s a dream away\\\\n\\\\nUnder this pressure, under this weight\\\\n\\\\nWe are diamonds \\\\n\\\\n\\\\n\\\\nI feel my heart beating\\\\n\\\\nI feel my heart underneath my skin\\\\n\\\\nI feel my heart beating\\\\n\\\\nOh, you make me feel\\\\n\\\\nLike I\\'m alive again\\\\n\\\\n\\\\n\\\\nAlive again!\\\\n\\\\n\\\\n\\\\nOh, you make me feel\\\\n\\\\nLike I\\'m alive again\\\\n\\\\n\\\\n\\\\nSaid I can\\'t go on, not in this way\\\\n\\\\nI\\'m a dream that died by light of day\\\\n\\\\nGonna hold up half the sky and say\\\\n\\\\nOnly I own me\\\\n\\\\n\\\\n\\\\nI feel my heart beating\\\\n\\\\nI feel my heart underneath my skin\\\\n\\\\nOh, I can feel my heart beating\\\\n\\\\n\\'Cause you make me feel\\\\n\\\\nLike I\\'m alive again\\\\n\\\\n\\\\n\\\\nAlive again!\\\\n\\\\n\\\\n\\\\nOh, you make me feel\\\\n\\\\nLike I\\'m alive again\\\\n\\\\n\\\\n\\\\nTurn your magic on, Umi she\\'d say\\\\n\\\\nEverything you want\\'s a dream away\\\\n\\\\nUnder this pressure, under this weight\\\\n\\\\nWe are diamonds taking shape\\\\n\\\\nWe are diamonds taking shape\\\\n\\\\n\\\\n\\\\n(Woo, woo)\\\\n\\\\n\\\\n\\\\nIf we\\'ve only got this life\\\\n\\\\nThis adventure, oh then I\\\\n\\\\nAnd if we\\'ve only got this life\\\\n\\\\nYou\\'ll get me through alive\\\\n\\\\nAnd if we\\'ve only got this life\\\\n\\\\nIn this adventure, oh then I\\\\n\\\\nWanna share it with you\\\\n\\\\nWith you, with you\\\\n\\\\nI said, oh, say oh\\\\n\\\\n\\\\n\\\\n(Woo hoo, woo hoo...)\"}'"
92 | ]
93 | },
94 | "execution_count": 4,
95 | "metadata": {},
96 | "output_type": "execute_result"
97 | }
98 | ],
99 | "source": [
100 | "r.text"
101 | ]
102 | },
103 | {
104 | "cell_type": "code",
105 | "execution_count": 5,
106 | "metadata": {},
107 | "outputs": [
108 | {
109 | "data": {
110 | "text/plain": [
111 | "{'lyrics': \"Turn your magic on, Umi she'd say\\r\\nEverything you want's a dream away\\r\\nWe are legends, every day\\r\\n\\nThat's what she told me\\r\\nTurn your magic on, to me she'd say\\n\\nEverything you want's a dream away\\n\\nUnder this pressure, under this weight\\n\\nWe are diamonds \\n\\n\\n\\nI feel my heart beating\\n\\nI feel my heart underneath my skin\\n\\nI feel my heart beating\\n\\nOh, you make me feel\\n\\nLike I'm alive again\\n\\n\\n\\nAlive again!\\n\\n\\n\\nOh, you make me feel\\n\\nLike I'm alive again\\n\\n\\n\\nSaid I can't go on, not in this way\\n\\nI'm a dream that died by light of day\\n\\nGonna hold up half the sky and say\\n\\nOnly I own me\\n\\n\\n\\nI feel my heart beating\\n\\nI feel my heart underneath my skin\\n\\nOh, I can feel my heart beating\\n\\n'Cause you make me feel\\n\\nLike I'm alive again\\n\\n\\n\\nAlive again!\\n\\n\\n\\nOh, you make me feel\\n\\nLike I'm alive again\\n\\n\\n\\nTurn your magic on, Umi she'd say\\n\\nEverything you want's a dream away\\n\\nUnder this pressure, under this weight\\n\\nWe are diamonds taking shape\\n\\nWe are diamonds taking shape\\n\\n\\n\\n(Woo, woo)\\n\\n\\n\\nIf we've only got this life\\n\\nThis adventure, oh then I\\n\\nAnd if we've only got this life\\n\\nYou'll get me through alive\\n\\nAnd if we've only got this life\\n\\nIn this adventure, oh then I\\n\\nWanna share it with you\\n\\nWith you, with you\\n\\nI said, oh, say oh\\n\\n\\n\\n(Woo hoo, woo hoo...)\"}"
112 | ]
113 | },
114 | "execution_count": 5,
115 | "metadata": {},
116 | "output_type": "execute_result"
117 | }
118 | ],
119 | "source": [
120 | "r.json()"
121 | ]
122 | },
123 | {
124 | "cell_type": "code",
125 | "execution_count": 6,
126 | "metadata": {},
127 | "outputs": [
128 | {
129 | "data": {
130 | "text/plain": [
131 | "\"Turn your magic on, Umi she'd say\\r\\nEverything you want's a dream away\\r\\nWe are legends, every day\\r\\n\\nThat's what she told me\\r\\nTurn your magic on, to me she'd say\\n\\nEverything you want's a dream away\\n\\nUnder this pressure, under this weight\\n\\nWe are diamonds \\n\\n\\n\\nI feel my heart beating\\n\\nI feel my heart underneath my skin\\n\\nI feel my heart beating\\n\\nOh, you make me feel\\n\\nLike I'm alive again\\n\\n\\n\\nAlive again!\\n\\n\\n\\nOh, you make me feel\\n\\nLike I'm alive again\\n\\n\\n\\nSaid I can't go on, not in this way\\n\\nI'm a dream that died by light of day\\n\\nGonna hold up half the sky and say\\n\\nOnly I own me\\n\\n\\n\\nI feel my heart beating\\n\\nI feel my heart underneath my skin\\n\\nOh, I can feel my heart beating\\n\\n'Cause you make me feel\\n\\nLike I'm alive again\\n\\n\\n\\nAlive again!\\n\\n\\n\\nOh, you make me feel\\n\\nLike I'm alive again\\n\\n\\n\\nTurn your magic on, Umi she'd say\\n\\nEverything you want's a dream away\\n\\nUnder this pressure, under this weight\\n\\nWe are diamonds taking shape\\n\\nWe are diamonds taking shape\\n\\n\\n\\n(Woo, woo)\\n\\n\\n\\nIf we've only got this life\\n\\nThis adventure, oh then I\\n\\nAnd if we've only got this life\\n\\nYou'll get me through alive\\n\\nAnd if we've only got this life\\n\\nIn this adventure, oh then I\\n\\nWanna share it with you\\n\\nWith you, with you\\n\\nI said, oh, say oh\\n\\n\\n\\n(Woo hoo, woo hoo...)\""
132 | ]
133 | },
134 | "execution_count": 6,
135 | "metadata": {},
136 | "output_type": "execute_result"
137 | }
138 | ],
139 | "source": [
140 | "r.json()['lyrics']"
141 | ]
142 | },
143 | {
144 | "cell_type": "code",
145 | "execution_count": null,
146 | "metadata": {},
147 | "outputs": [],
148 | "source": []
149 | }
150 | ],
151 | "metadata": {
152 | "kernelspec": {
153 | "display_name": "Python 3",
154 | "language": "python",
155 | "name": "python3"
156 | },
157 | "language_info": {
158 | "codemirror_mode": {
159 | "name": "ipython",
160 | "version": 3
161 | },
162 | "file_extension": ".py",
163 | "mimetype": "text/x-python",
164 | "name": "python",
165 | "nbconvert_exporter": "python",
166 | "pygments_lexer": "ipython3",
167 | "version": "3.6.5"
168 | }
169 | },
170 | "nbformat": 4,
171 | "nbformat_minor": 2
172 | }
173 |
--------------------------------------------------------------------------------
/section-4/prototyping/sine_wave_plot.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 6,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import matplotlib\n",
10 | "import matplotlib.pyplot as plt\n",
11 | "import numpy as np"
12 | ]
13 | },
14 | {
15 | "cell_type": "code",
16 | "execution_count": 7,
17 | "metadata": {},
18 | "outputs": [],
19 | "source": [
20 | "# Data for plotting\n",
21 | "t = np.arange(0.0, 2.0, 0.01)\n",
22 | "s = 1 + np.sin(2 * np.pi * t)"
23 | ]
24 | },
25 | {
26 | "cell_type": "code",
27 | "execution_count": 8,
28 | "metadata": {},
29 | "outputs": [
30 | {
31 | "data": {
32 | "image/png": "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\n",
33 | "text/plain": [
34 | ""
35 | ]
36 | },
37 | "metadata": {},
38 | "output_type": "display_data"
39 | }
40 | ],
41 | "source": [
42 | "# Note that using plt.subplots below is equivalent to using\n",
43 | "# fig = plt.figure() and then ax = fig.add_subplot(111)\n",
44 | "fig, ax = plt.subplots()\n",
45 | "ax.plot(t, s)\n",
46 | "ax.set(xlabel='time (s)', ylabel='voltage (mV)',\n",
47 | " title='About as simple as it gets, folks')\n",
48 | "ax.grid()\n",
49 | "plt.show()"
50 | ]
51 | }
52 | ],
53 | "metadata": {
54 | "kernelspec": {
55 | "display_name": "Python 3",
56 | "language": "python",
57 | "name": "python3"
58 | },
59 | "language_info": {
60 | "codemirror_mode": {
61 | "name": "ipython",
62 | "version": 3
63 | },
64 | "file_extension": ".py",
65 | "mimetype": "text/x-python",
66 | "name": "python",
67 | "nbconvert_exporter": "python",
68 | "pygments_lexer": "ipython3",
69 | "version": "3.6.5"
70 | }
71 | },
72 | "nbformat": 4,
73 | "nbformat_minor": 2
74 | }
75 |
--------------------------------------------------------------------------------
/section-5/lint/flickr_downloader/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-5/lint/flickr_downloader/__init__.py
--------------------------------------------------------------------------------
/section-5/lint/flickr_downloader/flickr_downloader.py:
--------------------------------------------------------------------------------
1 | """Download 100 interesting photos from Flickr"""
2 | import os
3 | import logging
4 | from concurrent.futures import ThreadPoolExecutor, as_completed
5 | import requests
6 |
7 |
8 | LOGGER = logging.getLogger(__name__)
9 | LOGGER.setLevel(logging.DEBUG)
10 |
11 |
12 | def __get_photo_list(api_key):
13 | url = "https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=%s&format=json&nojsoncallback=1" % api_key
14 |
15 | res = requests.get(url).json()
16 |
17 | return res
18 |
19 |
20 | def __download_photo(_photo):
21 | photo_url = "https://farm%s.staticflickr.com/%s/%s_%s.jpg" % (_photo["farm"], _photo["server"], _photo["id"], _photo["secret"])
22 |
23 | photo_res = requests.get(photo_url)
24 |
25 | return photo_res.content
26 |
27 |
28 | def __save_photo(_photo, _photo_content, location):
29 | with open(os.path.join(location, _photo["id"] + ".jpg"), "wb") as photo_file:
30 | photo_file.write(_photo_content)
31 |
32 |
33 | def download_interesting_photos(flickr_api_key, location):
34 | """Download 100 interesting Flickr photos
35 |
36 | Arguments:
37 | flickr_api_key -- the API key used to request the photos from Flickr
38 | location -- download location for the photos (must exist and be writable)
39 | """
40 | photos_list_res = __get_photo_list(flickr_api_key)
41 |
42 | photo_list = photos_list_res["photos"]["photo"]
43 |
44 | with ThreadPoolExecutor() as executor:
45 | future_photos = {executor.submit(__download_photo, photo): photo for photo in photo_list}
46 |
47 | for future in as_completed(future_photos):
48 | photo_content = future.result()
49 | __save_photo(future_photos[future], photo_content, location)
50 |
--------------------------------------------------------------------------------
/section-5/lint/pylintrc:
--------------------------------------------------------------------------------
1 | [MESSAGES CONTROL]
2 |
3 | disable=line-too-long
4 |
--------------------------------------------------------------------------------
/section-5/static-code-analysis/flickr_downloader/__init__.py:
--------------------------------------------------------------------------------
1 | from .flickr_downloader import FlickrDownloader, FlickrDownloaderException
2 |
--------------------------------------------------------------------------------
/section-5/static-code-analysis/flickr_downloader/flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import os
3 | import logging
4 | from concurrent.futures import ThreadPoolExecutor, as_completed
5 | from requests import RequestException
6 |
7 | logger = logging.getLogger(__name__)
8 | logger.setLevel(logging.DEBUG)
9 |
10 |
11 | class FlickrDownloaderException(Exception):
12 | pass
13 |
14 |
15 | class FlickrDownloader:
16 | def __init__(self, api_key, download_location):
17 | if not isinstance(api_key, str):
18 | raise FlickrDownloaderException("api_key should be str")
19 |
20 | if not isinstance(download_location, str):
21 | raise FlickrDownloaderException("download_location should be str")
22 |
23 | if api_key is None or len(api_key) == 0:
24 | raise FlickrDownloaderException("API Key is not set")
25 |
26 | if not os.path.exists(download_location) or not os.path.isdir(download_location):
27 | raise FlickrDownloaderException("Download location does not exist or is not a directory")
28 |
29 | self.__api_key = api_key
30 | self.__download_location = download_location
31 |
32 | def get_photo_list(self):
33 | url = "https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=%s&format=json&nojsoncallback=1" % self.__api_key
34 |
35 | try:
36 | res = requests.get(url).json()
37 | except RequestException as e:
38 | raise FlickrDownloaderException("Request error: %s", e)
39 |
40 | return res
41 |
42 | @staticmethod
43 | def download_photo(photo):
44 | try:
45 | photo_url = "https://farm%s.staticflickr.com/%s/%s_%s.jpg" % (photo["farm"], photo["server"], photo["id"], photo["secret"])
46 | except KeyError as e:
47 | raise FlickrDownloaderException("photo argument is invalid: %s" % e)
48 |
49 | try:
50 | photo_res = requests.get(photo_url)
51 | except RequestException as e:
52 | raise FlickrDownloaderException("Request error: %s", e)
53 |
54 | return photo_res.content
55 |
56 | def download_interesting_photos(self):
57 | photos_list_res = self.get_photo_list()
58 |
59 | photo_list = photos_list_res["photos"]["photo"]
60 |
61 | with ThreadPoolExecutor() as e:
62 | future_photos = {e.submit(self.download_photo, photo): photo for photo in photo_list}
63 |
64 | for future in as_completed(future_photos):
65 | photo_content = future.result()
66 | photo = future_photos[future]
67 | self.__save_photo(os.path.join(self.__download_location, photo["id"] + ".jpg"), photo_content)
68 |
69 | @staticmethod
70 | def __save_photo(filename, photo_content):
71 | try:
72 | with open(filename, "wb") as photo_file:
73 | photo_file.write(photo_content)
74 | except IOError as e:
75 | raise FlickrDownloaderException("Failed to save photo: %s", e)
76 |
--------------------------------------------------------------------------------
/section-5/static-code-analysis/pylintrc:
--------------------------------------------------------------------------------
1 | [MESSAGES CONTROL]
2 |
3 | disable=line-too-long
4 |
--------------------------------------------------------------------------------
/section-5/static-code-analysis/requirements.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | pytest-mock
3 | coverage
4 | pylint
5 | requests
6 |
--------------------------------------------------------------------------------
/section-5/static-code-analysis/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 |
4 | with open("README.md", "r") as readme_file:
5 | long_description = readme_file.read()
6 |
7 | setup(
8 | name="flickr_downloader",
9 | version="1.0.0-dev2",
10 | description="Python package that downloads 100 interesting photos from Flickr",
11 | long_description=long_description,
12 | long_description_content_type="text/markdown",
13 | author="Mihai Costea",
14 | license="MIT",
15 | classifiers=[
16 | "Development Status :: 4 - Beta",
17 | "Intended Audience :: Developers",
18 | "License :: OSI Approved :: MIT License"
19 | ],
20 | keywords="flickr photo downloader",
21 | packages=find_packages(),
22 | install_requires=["requests>=2"],
23 | python_requires="~=3.5"
24 | )
25 |
--------------------------------------------------------------------------------
/section-5/static-code-analysis/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.projectKey=python:flickr-downloader
2 | sonar.projectName=Flickr Downloader
3 | sonar.projectVersion=1.0
4 |
5 |
6 | sonar.sources=flickr_downloader
7 | sonar.tests=tests
8 | sonar.python.coverage.reportPath=coverage.xml
9 | sonar.python.xunit.reportPath=junit-result.xml
10 | sonar.python.pylint_config=pylintrc
11 | sonar.python.pylint=venv/bin/pylint
12 |
--------------------------------------------------------------------------------
/section-5/static-code-analysis/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-5/static-code-analysis/tests/__init__.py
--------------------------------------------------------------------------------
/section-5/static-code-analysis/tests/integration/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-5/static-code-analysis/tests/integration/__init__.py
--------------------------------------------------------------------------------
/section-5/static-code-analysis/tests/integration/test_flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flickr_downloader import FlickrDownloader
4 |
5 |
6 | class TestFlickrDownloaderIntegration(object):
7 | def test_get_get_photo_list(self, tmpdir):
8 | downloader = FlickrDownloader("{}".format(os.environ["FLICKR_API_KEY"]), str(tmpdir))
9 |
10 | photo_list = downloader.get_photo_list()
11 |
12 | assert "photos" in photo_list
13 | assert "photo" in photo_list["photos"]
14 |
15 | for photo in photo_list["photos"]["photo"]:
16 | assert "id" in photo
17 | assert "farm" in photo
18 | assert "server" in photo
19 | assert "id" in photo
20 | assert "secret" in photo
21 |
22 | def test_download_interesting_photos(self, tmpdir):
23 | downloader = FlickrDownloader("{}".format(os.environ["FLICKR_API_KEY"]), str(tmpdir))
24 |
25 | downloader.download_interesting_photos()
26 |
27 | assert len(os.listdir(tmpdir)) == 100
28 |
29 | for file in os.listdir(tmpdir):
30 | filename = os.fsdecode(file)
31 | assert filename.endswith(".jpg")
32 |
--------------------------------------------------------------------------------
/section-5/static-code-analysis/tests/test_flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 | import requests
5 | from requests import RequestException
6 |
7 | from flickr_downloader import FlickrDownloader, FlickrDownloaderException
8 |
9 |
10 | class TestFlickrDownloader(object):
11 | def test_init_correct(self, tmpdir):
12 | try:
13 | FlickrDownloader("testkey", str(tmpdir))
14 | except FlickrDownloaderException as e:
15 | pytest.fail(e)
16 |
17 | def test_init_missing_download_folder(self):
18 | with pytest.raises(FlickrDownloaderException):
19 | FlickrDownloader("testkey", "amissingfolder")
20 |
21 | def test_init_download_folder_not_a_folder(self, tmpdir):
22 | f = tmpdir.join("afile.txt")
23 | f.write("test text")
24 |
25 | with pytest.raises(FlickrDownloaderException):
26 | FlickrDownloader("testkey", str(f))
27 |
28 | def test_init_api_key_none(self, tmpdir):
29 | with pytest.raises(FlickrDownloaderException):
30 | FlickrDownloader(None, str(tmpdir))
31 |
32 | def test_init_api_key_empty(self, tmpdir):
33 | with pytest.raises(FlickrDownloaderException):
34 | FlickrDownloader("", str(tmpdir))
35 |
36 | def test_get_photo_list_correct_url(self, tmpdir, mocker):
37 | response_mock = mocker.MagicMock()
38 | response_mock.json = mocker.MagicMock(return_value="valid json here")
39 |
40 | mocker.patch.object(requests, "get", return_value=response_mock)
41 |
42 | downloader = FlickrDownloader("test_key", str(tmpdir))
43 |
44 | photo_list = downloader.get_photo_list()
45 |
46 | assert photo_list == "valid json here"
47 |
48 | url = "https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=%s&format=json&nojsoncallback=1" % "test_key"
49 |
50 | requests.get.assert_called_once_with(url)
51 |
52 | def test_get_photo_list_failed_request(self, tmpdir, mocker):
53 | mocker.patch.object(requests, "get", side_effect=RequestException("test exception"))
54 |
55 | downloader = FlickrDownloader("test_key", str(tmpdir))
56 |
57 | with pytest.raises(FlickrDownloaderException):
58 | downloader.get_photo_list()
59 |
60 | def test_download_photo_correct(self, tmpdir, mocker):
61 | response_mock = mocker.MagicMock()
62 | response_mock.content = "test photo content"
63 |
64 | mocker.patch.object(requests, "get", return_value=response_mock)
65 |
66 | downloader = FlickrDownloader("test_key", str(tmpdir))
67 |
68 | photo = {
69 | "farm": "testfarm",
70 | "server": "testserver",
71 | "id": "testid",
72 | "secret": "testsecret"
73 | }
74 |
75 | photo_content = downloader.download_photo(photo.copy())
76 |
77 | assert photo_content == "test photo content"
78 | photo_url = "https://farm%s.staticflickr.com/%s/%s_%s.jpg" % (photo["farm"], photo["server"], photo["id"], photo["secret"])
79 | requests.get.assert_called_once_with(photo_url)
80 |
81 | def test_download_photo_missing_keys(self, tmpdir, mocker):
82 | response_mock = mocker.MagicMock()
83 | response_mock.content = "test photo content"
84 |
85 | mocker.patch.object(requests, "get", return_value=response_mock)
86 |
87 | downloader = FlickrDownloader("test_key", str(tmpdir))
88 |
89 | photo = {
90 | "server": "testserver",
91 | "id": "testid",
92 | "secret": "testsecret"
93 | }
94 |
95 | with pytest.raises(FlickrDownloaderException):
96 | downloader.download_photo(photo)
97 |
98 | photo = {
99 | "farm": "testfarm",
100 | "id": "testid",
101 | "secret": "testsecret"
102 | }
103 |
104 | with pytest.raises(FlickrDownloaderException):
105 | downloader.download_photo(photo)
106 |
107 | photo = {
108 | "farm": "testfarm",
109 | "server": "testserver",
110 | "secret": "testsecret"
111 | }
112 |
113 | with pytest.raises(FlickrDownloaderException):
114 | downloader.download_photo(photo)
115 |
116 | photo = {
117 | "farm": "testfarm",
118 | "server": "testserver",
119 | "id": "testid"
120 | }
121 |
122 | with pytest.raises(FlickrDownloaderException):
123 | downloader.download_photo(photo)
124 |
125 | def test_download_photo_failed_request(self, tmpdir, mocker):
126 | mocker.patch.object(requests, "get", side_effect=RequestException("test exception"))
127 | downloader = FlickrDownloader("test_key", str(tmpdir))
128 |
129 | photo = {
130 | "farm": "testfarm",
131 | "server": "testserver",
132 | "id": "testid",
133 | "secret": "testsecret"
134 | }
135 |
136 | with pytest.raises(FlickrDownloaderException):
137 | downloader.download_photo(photo)
138 |
139 | def test_download_interesting_photos_correct(self, tmpdir, mocker):
140 | photos_list_mock = {
141 | "photos": {
142 | "photo": [
143 | {
144 | "id": "1"
145 | },
146 | {
147 | "id": "2"
148 | },
149 | {
150 | "id": "3"
151 | }
152 | ]
153 | }
154 | }
155 |
156 | # Mock the downloader class
157 | get_photo_list_mock = mocker.patch.object(FlickrDownloader, "get_photo_list")
158 | download_photo_mock = mocker.patch.object(FlickrDownloader, "download_photo")
159 | save_photo_mock = mocker.patch.object(FlickrDownloader, "_FlickrDownloader__save_photo")
160 |
161 | get_photo_list_mock.return_value = photos_list_mock
162 | download_photo_mock.return_value = "photo content"
163 |
164 | downloader = FlickrDownloader("test_key", str(tmpdir))
165 |
166 | downloader.download_interesting_photos()
167 |
168 | get_photo_list_mock.assert_called_once()
169 | download_photo_mock.assert_any_call({"id": "1"})
170 | download_photo_mock.assert_any_call({"id": "2"})
171 | download_photo_mock.assert_any_call({"id": "3"})
172 | save_photo_mock.assert_any_call(os.path.join(str(tmpdir), "1.jpg"), "photo content")
173 | save_photo_mock.assert_any_call(os.path.join(str(tmpdir), "2.jpg"), "photo content")
174 | save_photo_mock.assert_any_call(os.path.join(str(tmpdir), "3.jpg"), "photo content")
175 |
--------------------------------------------------------------------------------
/section-5/types/flickr_downloader/__init__.py:
--------------------------------------------------------------------------------
1 | from .flickr_downloader import FlickrDownloader, FlickrDownloaderException
2 |
--------------------------------------------------------------------------------
/section-5/types/flickr_downloader/flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import os
3 | import logging
4 | from concurrent.futures import ThreadPoolExecutor, as_completed
5 | from typing import Dict
6 | from requests import RequestException
7 |
8 | logger = logging.getLogger(__name__)
9 | logger.setLevel(logging.DEBUG)
10 |
11 |
12 | class FlickrDownloaderException(Exception):
13 | pass
14 |
15 |
16 | class FlickrDownloader:
17 | def __init__(self, api_key: str, download_location: str) -> None:
18 | if api_key is None or len(api_key) == 0:
19 | raise FlickrDownloaderException("API Key is not set")
20 |
21 | if not os.path.exists(download_location) or not os.path.isdir(download_location):
22 | raise FlickrDownloaderException("Download location does not exist or is not a directory")
23 |
24 | self.__api_key = api_key
25 | self.__download_location = download_location
26 |
27 | def get_photo_list(self) -> Dict[str, object]:
28 | url = "https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=%s&format=json&nojsoncallback=1" % self.__api_key
29 |
30 | try:
31 | res = requests.get(url).json()
32 | except RequestException as e:
33 | raise FlickrDownloaderException("Request error: %s", e)
34 |
35 | return res
36 |
37 | @staticmethod
38 | def download_photo(photo: Dict[str, object]) -> bytes:
39 | try:
40 | photo_url = "https://farm%s.staticflickr.com/%s/%s_%s.jpg" % (photo["farm"], photo["server"], photo["id"], photo["secret"])
41 | except KeyError as e:
42 | raise FlickrDownloaderException("photo argument is invalid: %s" % e)
43 |
44 | try:
45 | photo_res = requests.get(photo_url)
46 | except RequestException as e:
47 | raise FlickrDownloaderException("Request error: %s", e)
48 |
49 | return photo_res.content
50 |
51 | def download_interesting_photos(self):
52 | photos_list_res = self.get_photo_list()
53 |
54 | photo_list = photos_list_res["photos"]["photo"]
55 |
56 | with ThreadPoolExecutor() as e:
57 | future_photos = {e.submit(self.download_photo, photo): photo for photo in photo_list}
58 |
59 | for future in as_completed(future_photos):
60 | photo_content = future.result()
61 | photo = future_photos[future]
62 | self.__save_photo(os.path.join(self.__download_location, photo["id"] + ".jpg"), photo_content)
63 |
64 | @staticmethod
65 | def __save_photo(filename: str, photo_content: bytes):
66 | try:
67 | with open(filename, "wb") as photo_file:
68 | photo_file.write(photo_content)
69 | except IOError as e:
70 | raise FlickrDownloaderException("Failed to save photo: %s", e)
71 |
--------------------------------------------------------------------------------
/section-5/types/pylintrc:
--------------------------------------------------------------------------------
1 | [MESSAGES CONTROL]
2 |
3 | disable=line-too-long
4 |
--------------------------------------------------------------------------------
/section-5/types/requirements.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | pytest-mock
3 | coverage
4 | pylint
5 | requests
6 |
--------------------------------------------------------------------------------
/section-5/types/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 |
4 | with open("README.md", "r") as readme_file:
5 | long_description = readme_file.read()
6 |
7 | setup(
8 | name="flickr_downloader",
9 | version="1.0.0-dev2",
10 | description="Python package that downloads 100 interesting photos from Flickr",
11 | long_description=long_description,
12 | long_description_content_type="text/markdown",
13 | author="Mihai Costea",
14 | license="MIT",
15 | classifiers=[
16 | "Development Status :: 4 - Beta",
17 | "Intended Audience :: Developers",
18 | "License :: OSI Approved :: MIT License"
19 | ],
20 | keywords="flickr photo downloader",
21 | packages=find_packages(),
22 | install_requires=["requests>=2"],
23 | python_requires="~=3.5"
24 | )
25 |
--------------------------------------------------------------------------------
/section-5/types/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.projectKey=python:flickr-downloader
2 | sonar.projectName=Flickr Downloader
3 | sonar.projectVersion=1.0
4 |
5 |
6 | sonar.sources=flickr_downloader
7 | sonar.python.coverage.reportPath=coverage.xml
8 | sonar.python.xunit.reportPath=junit-report.xml
9 | sonar.python.pylint_config=pylintrc
10 | sonar.python.pylint=venv/bin/pylint
11 |
--------------------------------------------------------------------------------
/section-5/types/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-5/types/tests/__init__.py
--------------------------------------------------------------------------------
/section-5/types/tests/integration/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PacktPublishing/Python-Application-Development-Tips-Tricks-and-Techniques/fa347ad227965329c84922ea7149521ae1fb6868/section-5/types/tests/integration/__init__.py
--------------------------------------------------------------------------------
/section-5/types/tests/integration/test_flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flickr_downloader import FlickrDownloader
4 |
5 |
6 | class TestFlickrDownloaderIntegration(object):
7 | def test_get_get_photo_list(self, tmpdir):
8 | downloader = FlickrDownloader("{}".format(os.environ["FLICKR_API_KEY"]), str(tmpdir))
9 |
10 | photo_list = downloader.get_photo_list()
11 |
12 | assert "photos" in photo_list
13 | assert "photo" in photo_list["photos"]
14 |
15 | for photo in photo_list["photos"]["photo"]:
16 | assert "id" in photo
17 | assert "farm" in photo
18 | assert "server" in photo
19 | assert "id" in photo
20 | assert "secret" in photo
21 |
22 | def test_download_interesting_photos(self, tmpdir):
23 | downloader = FlickrDownloader("{}".format(os.environ["FLICKR_API_KEY"]), str(tmpdir))
24 |
25 | downloader.download_interesting_photos()
26 |
27 | assert len(os.listdir(tmpdir)) == 100
28 |
29 | for file in os.listdir(tmpdir):
30 | filename = os.fsdecode(file)
31 | assert filename.endswith(".jpg")
32 |
--------------------------------------------------------------------------------
/section-5/types/tests/test_flickr_downloader.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 | import requests
5 | from requests import RequestException
6 |
7 | from flickr_downloader import FlickrDownloader, FlickrDownloaderException
8 |
9 |
10 | class TestFlickrDownloader(object):
11 | def test_init_correct(self, tmpdir):
12 | try:
13 | FlickrDownloader("testkey", str(tmpdir))
14 | except FlickrDownloaderException as e:
15 | pytest.fail(e)
16 |
17 | def test_init_missing_download_folder(self):
18 | with pytest.raises(FlickrDownloaderException):
19 | FlickrDownloader("testkey", "amissingfolder")
20 |
21 | def test_init_download_folder_not_a_folder(self, tmpdir):
22 | f = tmpdir.join("afile.txt")
23 | f.write("test text")
24 |
25 | with pytest.raises(FlickrDownloaderException):
26 | FlickrDownloader("testkey", str(f))
27 |
28 | def test_init_api_key_none(self, tmpdir):
29 | with pytest.raises(FlickrDownloaderException):
30 | FlickrDownloader(None, str(tmpdir))
31 |
32 | def test_init_api_key_empty(self, tmpdir):
33 | with pytest.raises(FlickrDownloaderException):
34 | FlickrDownloader("", str(tmpdir))
35 |
36 | def test_get_photo_list_correct_url(self, tmpdir, mocker):
37 | response_mock = mocker.MagicMock()
38 | response_mock.json = mocker.MagicMock(return_value="valid json here")
39 |
40 | mocker.patch.object(requests, "get", return_value=response_mock)
41 |
42 | downloader = FlickrDownloader("test_key", str(tmpdir))
43 |
44 | photo_list = downloader.get_photo_list()
45 |
46 | assert photo_list == "valid json here"
47 |
48 | url = "https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=%s&format=json&nojsoncallback=1" % "test_key"
49 |
50 | requests.get.assert_called_once_with(url)
51 |
52 | def test_get_photo_list_failed_request(self, tmpdir, mocker):
53 | mocker.patch.object(requests, "get", side_effect=RequestException("test exception"))
54 |
55 | downloader = FlickrDownloader("test_key", str(tmpdir))
56 |
57 | with pytest.raises(FlickrDownloaderException):
58 | downloader.get_photo_list()
59 |
60 | def test_download_photo_correct(self, tmpdir, mocker):
61 | response_mock = mocker.MagicMock()
62 | response_mock.content = "test photo content"
63 |
64 | mocker.patch.object(requests, "get", return_value=response_mock)
65 |
66 | downloader = FlickrDownloader("test_key", str(tmpdir))
67 |
68 | photo = {
69 | "farm": "testfarm",
70 | "server": "testserver",
71 | "id": "testid",
72 | "secret": "testsecret"
73 | }
74 |
75 | photo_content = downloader.download_photo(photo.copy())
76 |
77 | assert photo_content == "test photo content"
78 | photo_url = "https://farm%s.staticflickr.com/%s/%s_%s.jpg" % (photo["farm"], photo["server"], photo["id"], photo["secret"])
79 | requests.get.assert_called_once_with(photo_url)
80 |
81 | def test_download_photo_missing_keys(self, tmpdir, mocker):
82 | response_mock = mocker.MagicMock()
83 | response_mock.content = "test photo content"
84 |
85 | mocker.patch.object(requests, "get", return_value=response_mock)
86 |
87 | downloader = FlickrDownloader("test_key", str(tmpdir))
88 |
89 | photo = {
90 | "server": "testserver",
91 | "id": "testid",
92 | "secret": "testsecret"
93 | }
94 |
95 | with pytest.raises(FlickrDownloaderException):
96 | downloader.download_photo(photo)
97 |
98 | photo = {
99 | "farm": "testfarm",
100 | "id": "testid",
101 | "secret": "testsecret"
102 | }
103 |
104 | with pytest.raises(FlickrDownloaderException):
105 | downloader.download_photo(photo)
106 |
107 | photo = {
108 | "farm": "testfarm",
109 | "server": "testserver",
110 | "secret": "testsecret"
111 | }
112 |
113 | with pytest.raises(FlickrDownloaderException):
114 | downloader.download_photo(photo)
115 |
116 | photo = {
117 | "farm": "testfarm",
118 | "server": "testserver",
119 | "id": "testid"
120 | }
121 |
122 | with pytest.raises(FlickrDownloaderException):
123 | downloader.download_photo(photo)
124 |
125 | def test_download_photo_failed_request(self, tmpdir, mocker):
126 | mocker.patch.object(requests, "get", side_effect=RequestException("test exception"))
127 | downloader = FlickrDownloader("test_key", str(tmpdir))
128 |
129 | photo = {
130 | "farm": "testfarm",
131 | "server": "testserver",
132 | "id": "testid",
133 | "secret": "testsecret"
134 | }
135 |
136 | with pytest.raises(FlickrDownloaderException):
137 | downloader.download_photo(photo)
138 |
139 | def test_download_interesting_photos_correct(self, tmpdir, mocker):
140 | photos_list_mock = {
141 | "photos": {
142 | "photo": [
143 | {
144 | "id": "1"
145 | },
146 | {
147 | "id": "2"
148 | },
149 | {
150 | "id": "3"
151 | }
152 | ]
153 | }
154 | }
155 |
156 | # Mock the downloader class
157 | get_photo_list_mock = mocker.patch.object(FlickrDownloader, "get_photo_list")
158 | download_photo_mock = mocker.patch.object(FlickrDownloader, "download_photo")
159 | save_photo_mock = mocker.patch.object(FlickrDownloader, "_FlickrDownloader__save_photo")
160 |
161 | get_photo_list_mock.return_value = photos_list_mock
162 | download_photo_mock.return_value = "photo content"
163 |
164 | downloader = FlickrDownloader("test_key", str(tmpdir))
165 |
166 | downloader.download_interesting_photos()
167 |
168 | get_photo_list_mock.assert_called_once()
169 | download_photo_mock.assert_any_call({"id": "1"})
170 | download_photo_mock.assert_any_call({"id": "2"})
171 | download_photo_mock.assert_any_call({"id": "3"})
172 | save_photo_mock.assert_any_call(os.path.join(str(tmpdir), "1.jpg"), "photo content")
173 | save_photo_mock.assert_any_call(os.path.join(str(tmpdir), "2.jpg"), "photo content")
174 | save_photo_mock.assert_any_call(os.path.join(str(tmpdir), "3.jpg"), "photo content")
175 |
--------------------------------------------------------------------------------