├── .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 |
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 | --------------------------------------------------------------------------------