├── .editorconfig
├── 03_dekoratory
├── Dekoratory.ipynb
└── praca_domowa
│ ├── README.MD
│ └── rozwiazanie
│ ├── Pipfile
│ ├── Pipfile.lock
│ ├── resolution.py
│ └── tests.py
├── 04_sqlite
├── cala_baza.png
├── nasza_baza.png
├── praca_domowa
│ ├── README.md
│ └── rozwiazanie
│ │ ├── Pipfile
│ │ ├── Procfile
│ │ ├── app.py
│ │ └── migracja.sql
├── przyklady
│ ├── Procfile
│ ├── app.py
│ ├── prepare_db.py
│ ├── requirements.txt
│ └── templates
│ │ ├── actors.html
│ │ ├── actors_count.html
│ │ ├── add_actor.html
│ │ ├── add_actor_sqli.html
│ │ ├── delete_actor.html
│ │ ├── edit_actor.html
│ │ ├── films.html
│ │ ├── films_with_category.html
│ │ ├── main.html
│ │ └── single_film.html
└── sqlite.ipynb
├── 05_sqlalchemy
├── README.MD
└── sqlalchemy.ipynb
├── 06_asyncio
├── asyncio.ipynb
└── asyncio_request_demo.py
├── 07_celery
├── celery.ipynb
├── przyklady
│ ├── tasks.py
│ └── web_app
│ │ ├── Procfile
│ │ ├── app.py
│ │ ├── requirements.txt
│ │ └── templates
│ │ └── add_user.html
├── twitter_use_case.png
├── web_application_flow.png
└── web_application_flow_celery.png
├── README.md
├── logo.png
├── plan_zajec.png
├── zadania_rekrutacyjne
├── Zadanie_1
│ ├── Zadanie_1_polecenie.md
│ └── zadanie_1_words.zip
├── Zadanie_2
│ └── Zadanie_2_polecenie.md
├── Zadanie_3
│ ├── Zadanie_3_polecenie.md
│ └── zadanie_3_triangle_small.txt
└── Zadanie_4
│ ├── Zadanie_4_polecenie.md
│ └── zadanie_4_triangle_big.txt
├── zajecia_1
├── README.md
├── praca_domowa
│ ├── README.md
│ └── rozwiazanie
│ │ └── hello-world.py
├── warszat_01.ipynb
└── web_app
│ ├── Procfile
│ ├── app.py
│ └── requirements.txt
└── zajecia_2
├── Jeszcze_jedna.ipynb
├── hello_flask.py
├── praca_domowa
├── README.MD
└── rozwiazanie
│ ├── Pipfile
│ ├── Pipfile.lock
│ ├── Procfile
│ ├── Procfile.local
│ ├── app.py
│ ├── errors.py
│ └── templates
│ └── greeting.html
└── templates
├── cookies_tmpl.html
├── person_tmpl.html
├── querystring_render_tmpl.html
└── route_description_tmpl.html
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{md,markdown}]
2 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/03_dekoratory/praca_domowa/README.MD:
--------------------------------------------------------------------------------
1 | # Praca domowa 3
2 |
3 |
4 | Odpowiedź podaj **wyłącznie** przez: https://goo.gl/forms/0ORFf46sjdDoyPW23
5 |
6 | Na odpowiedzi czekamy do 27.03.2018 23:59:59 czasu polskiego.
7 |
8 | ## Zadanie 1.
9 | Napisz dekorator `@add_tag`, który opakowuje funkcje zwracające tekst, otaczając wynik podanym stringiem zamkniętym w klamry (jak w html), w formie `wynik`.
10 | Przykład:
11 | ```python
12 | @add_tag('h1')
13 | def write_something():
14 | return 'something'
15 |
16 | result = write_something()
17 | assert result == '
something
'
18 | ```
19 |
20 | ## Zadanie 2.
21 | Napisz walidator JSON'ów `@validate_json`. Ma on sprawdzać, czy przekazany json zawiera tylko i wyłącznie wymienione w argumentach dekoratora elementy. W przypadku innej zawartości niech rzuca `ValueError`.
22 |
23 | ```python
24 | @validate_json('first_name', 'last_name')
25 | def process_json(json_data):
26 | return len(json_data)
27 |
28 | result = process_json('{"first_name": "James", "last_name": "Bond"}')
29 | assert result == 44
30 |
31 | process_json('{"first_name": "James", "age": 45}')
32 | > ValueError
33 |
34 | process_json('{"first_name": "James", "last_name": "Bond", "age": 45}')
35 | > ValueError
36 | ```
37 |
38 | ## Zadanie 3.
39 | Napisz dekorator `@log_this` który będzie logować wywołania funkcji.
40 | Do dekoratora przekazujemy:
41 | * `logger` na którym będziemy logować.
42 | Załadamy, że `logger` ma ustawiony poziom na logowania na `DEBUG`
43 | * `level` poziom logowania z jakim będzie wywołany `logger`.
44 | * `format` format w jakim będziemy logować wywołania.
45 |
46 | `@log_this` ma drukować w (w odpowiednim formacie) kolejności:
47 | * poziom logowania* nazwę funkcji
48 | * argumenty wywołania
49 | * wynik funkcji
50 |
51 |
52 | Poniżej przykład
53 |
54 | Zakładając taki kod:
55 | ```python
56 | import logging
57 |
58 | logger = logging.getLogger(__name__)
59 | logger.setLevel(logging.DEBUG)
60 |
61 | @log_this(logger, level=logging.INFO, format='%s: %s -> %s')
62 | def my_func(a, b, c=None, d=False):
63 | return 'Wow!'
64 | ```
65 |
66 | Po takim wywołaniu:
67 | ```
68 | my_func(1, 2, d=True)
69 | ```
70 |
71 | Zawoła na loggerze:
72 | ```
73 | logger.info('%s: %s -> %s', 'my_func', ('1', '2', 'd=True'), 'Wow!')
74 | ```
75 |
76 | > Do rozwiązania zadania przyda się wiedza stąd: https://docs.python.org/3.6/library/logging.html.
77 |
--------------------------------------------------------------------------------
/03_dekoratory/praca_domowa/rozwiazanie/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.python.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 |
8 | [dev-packages]
9 | pytest = "*"
10 |
11 | [requires]
12 | python_version = "3.6"
13 |
--------------------------------------------------------------------------------
/03_dekoratory/praca_domowa/rozwiazanie/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "fbab11392a0e9a1a0fde8646dfc2559e7311ea367d2c6f660695e6d009db4635"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.6"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.python.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {},
19 | "develop": {
20 | "attrs": {
21 | "hashes": [
22 | "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9",
23 | "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450"
24 | ],
25 | "version": "==17.4.0"
26 | },
27 | "more-itertools": {
28 | "hashes": [
29 | "sha256:0dd8f72eeab0d2c3bd489025bb2f6a1b8342f9b198f6fc37b52d15cfa4531fea",
30 | "sha256:11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2442e",
31 | "sha256:c9ce7eccdcb901a2c75d326ea134e0886abfbea5f93e91cc95de9507c0816c44"
32 | ],
33 | "version": "==4.1.0"
34 | },
35 | "pluggy": {
36 | "hashes": [
37 | "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff"
38 | ],
39 | "version": "==0.6.0"
40 | },
41 | "py": {
42 | "hashes": [
43 | "sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881",
44 | "sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a"
45 | ],
46 | "version": "==1.5.3"
47 | },
48 | "pytest": {
49 | "hashes": [
50 | "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c",
51 | "sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1"
52 | ],
53 | "index": "pypi",
54 | "version": "==3.5.0"
55 | },
56 | "six": {
57 | "hashes": [
58 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
59 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
60 | ],
61 | "version": "==1.11.0"
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/03_dekoratory/praca_domowa/rozwiazanie/resolution.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from functools import wraps
3 | from itertools import chain
4 | from json import loads
5 | from typing import Callable, Any
6 |
7 |
8 | def add_tag(tag):
9 | """Add a specified tag for the function that returns strings"""
10 | def decorator(func: Callable[[Any], str]):
11 | @wraps(func)
12 | def wrapper(*args, **kwargs):
13 | return f'<{tag}>{func(*args, **kwargs)}{tag}>'
14 | return wrapper
15 | return decorator
16 |
17 |
18 | def validate_json(*required_keys):
19 | """Validates first json"""
20 | def decorator(func: Callable[[Any], Any]):
21 | @wraps(func)
22 | def wrapper(input_json, *args, **kwargs):
23 | input_dict = loads(input_json)
24 | # if not all(key in input_dict for key in required_keys):
25 | if input_dict.keys() != set(required_keys):
26 | raise ValueError
27 | return func(input_json, *args, **kwargs)
28 | return wrapper
29 | return decorator
30 |
31 |
32 | def log_this(logger: logging.Logger, level, fmt):
33 | """Logs execution"""
34 | def decorator(func):
35 | @wraps(func)
36 | def wrapper(*args, **kwargs):
37 | rv = func(*args, **kwargs)
38 | logger.log(fmt, func.__name__,
39 | tuple(chain((str(a) for a in args),
40 | (f'{k}={v}' for k, v in kwargs.items()))),
41 | str(rv))
42 | return rv
43 | return wrapper
44 | return decorator
--------------------------------------------------------------------------------
/03_dekoratory/praca_domowa/rozwiazanie/tests.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from unittest.mock import MagicMock
3 |
4 | from pytest import raises, mark
5 |
6 | from resolution import add_tag, validate_json, log_this
7 |
8 |
9 | def test_add_tag():
10 | @add_tag('h1')
11 | def hello():
12 | return('hello')
13 | assert hello() == 'hello
'
14 |
15 |
16 | @mark.parametrize('required_keys, test_keys, should_throw', [
17 | [
18 | ['first_name', 'second_name'],
19 | '{"first_name": "James", "second_name": "Bond"}',
20 | False,
21 | ],
22 | [
23 | ['first_name', 'second_name'], '{"first_name": "James"}',
24 | True,
25 | ],
26 | [
27 | ['first_name', 'second_name'],
28 | '{"first_name": "James", "second_name": "Bond", "agent_id": "007"}',
29 | True,
30 | ],
31 | ])
32 | def test_validate_json(required_keys, test_keys, should_throw):
33 |
34 | @validate_json(*required_keys)
35 | def hello(json_input):
36 | pass
37 |
38 | if should_throw:
39 | with raises(ValueError):
40 | hello(test_keys)
41 | else:
42 | hello(test_keys)
43 |
44 |
45 | def test_log_this():
46 |
47 | logger = MagicMock()
48 |
49 | @log_this(logger, level=logging.INFO, fmt='%s: %s -> %s')
50 | def my_func(a, b, c=None, d=False):
51 | return 'Wow!'
52 |
53 | my_func(1, 2, d=True)
54 |
55 | logger.log.assert_called()
56 | logger.log.assert_called_once()
57 | logger.log.assert_called_with('%s: %s -> %s', 'my_func',
58 | ('1', '2', 'd=True'), 'Wow!')
59 |
--------------------------------------------------------------------------------
/04_sqlite/cala_baza.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DaftAcademy/python_levelup_2018/d05320ee90af11fff1ae5e2c24cead6fca9e1abf/04_sqlite/cala_baza.png
--------------------------------------------------------------------------------
/04_sqlite/nasza_baza.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DaftAcademy/python_levelup_2018/d05320ee90af11fff1ae5e2c24cead6fca9e1abf/04_sqlite/nasza_baza.png
--------------------------------------------------------------------------------
/04_sqlite/praca_domowa/README.md:
--------------------------------------------------------------------------------
1 | # Zadanie domowe - SQLite
2 |
3 | Wszystkie podpunkty należy rozwiązać z wykorzystaniem testowej bazy danych Sakila (więcej o bazie Sakila w materiałach do zajęć). Dla celów testów istotne jest, żeby wasza baza zawierała dokładnie te same dane co na zajęciach. Także jeśli będziecie robić jakieś eksperymenty w trakcie rozwiązywania zadań, to pamiętajcie o wrzuceniu czystej bazy na koniec. Pamiętajcie też o uwagach Kuby z wykładu odnośnie działania SQLite na heroku.
4 |
5 | Na rozwiązania czekamy do wtorku 10.04.2018 23:59:59 CEST
6 |
7 | Rozwiązania prosimy przesyłać tylko za pośrednictwem formularza: https://goo.gl/forms/7lzl6pfnSwUgLlhz1
8 |
9 | Prosimy o zadawanie pytań poprzez issue na GitHubie.
10 |
11 |
12 | 1. Stwórz endpoint, który zwróci w JSON listę wszystkich nazw miast zawartych w tabelli `city`. Kolejność nazw miast powinna być alfabetyczna.
13 | Zapytanie na:
14 | ```
15 | GET /cities
16 | ```
17 | zwróci:
18 | ```json
19 | ["A Corua (La Corua)", "Alessandria", ...]
20 | ```
21 | 2. Dodaj do endpointu wyświetlającego listę nazw miast obsługę parametru `country_name`. Parametr ma powodować wybranie miast tylko z podanego kraju. Kolejność nazw miast powinna być alfabetyczna tak jak w pkt. 1.
22 | ```
23 | GET /cities?country_name=Poland
24 | ```
25 | zwróci:
26 | ```json
27 | [
28 | "Bydgoszcz",
29 | "Czestochowa",
30 | "Jastrzebie-Zdrj",
31 | "Kalisz",
32 | "Lublin",
33 | "Plock",
34 | "Tychy",
35 | "Wroclaw"
36 | ]
37 | ```
38 | 3. Przygotuj endpoint, który zwróci w JSON słownik gdzie kluczem będzie nazwa języka, a wartością liczba ról we wszystkich filmach w danym języku. Zakładamy, że dany aktor grał tylko jedną rolę w danym filmie.
39 | Wskazówka: `GROUP BY`: https://www.sqlite.org/lang_select.html#resultset
40 | Wskazówka: agregacja
41 |
42 | zapytanie:
43 | ```
44 | GET /lang_roles
45 | ```
46 |
47 | Przykładowa odpowiedź:
48 | ```json
49 | {
50 | "English": 1092400,
51 | "Italian": 0,
52 | "Japanese": 0,
53 | "Mandarin": 0,
54 | "German": 0,
55 | "French": 0
56 | }
57 | ```
58 | 4. Przygotuj endpoint do dodawania nowych miast. W JSONie przesyłanym POSTem powinna znajdować się informacja o nazwie miasta oraz id kraju do którego należy dodawane miasto. Po dodaniu miasta należy zwrócić stworzony obiekt z kodem 200. Endpoint powinien zawierać prostą walidację (tzn. odrzucać próbę stworzenia miasta dla nieistniejącego kraju itp.). W przypadku błędu należy zwrócić kod 400 oraz JSONa z kluczem "error", który będzie zawierał krótki opis błędu (treść błędu nie będzie sprawdzana).
59 |
60 | ```
61 | POST /cities
62 |
63 | {
64 | "country_id": 76,
65 | "city_name": "Warszawa"
66 | }
67 | ```
68 | Przykładowa odpowiedź z sukcesem:
69 | ```json
70 | {
71 | "country_id": 76,
72 | "city_name": "Warszawa",
73 | "city_id": 601
74 | }
75 | ```
76 | Przykładowa odpowiedź z błędem:
77 | ```json
78 | {
79 | "error": "Invalid country_id"
80 | }
81 | ```
82 |
83 | 5. Dodaj do endpointu `GET /cities` możliwość dzielenia wyniku na strony. Endpoint powinien obsługiwać dodatkowe parametry w query stringu - `per_page`, czyli ile wyników ma się wyświetlać na jednej stronie i `page`, który mówi o tym, którą stronę chcemy aktualnie wyświetlić. Strony numerujemy od 1 w górę. Poprawne rozwiązanie powinno działać razem z filtrowaniem po nazwie kraju jeśli nazwa kraju będzie podana.
84 | Wskazówka: `LIMIT`
85 | Wskazówka: `OFFSET`
86 |
87 | ```
88 | GET /cities?per_page=10&page=2
89 | ```
90 | zwróci:
91 | ```json
92 | [
93 | "Akron",
94 | "Alessandria",
95 | "Allappuzha (Alleppey)",
96 | "Allende",
97 | "Almirante Brown",
98 | "Alvorada",
99 | "Ambattur",
100 | "Amersfoort",
101 | "Amroha",
102 | "Angra dos Reis"
103 | ]
104 | ```
105 |
106 | 6*. Na wykładzie Kuba wspominał o tym, że relacja wiele do wielu między tabelami `film` i `category` jest trochę na wyrost. Analiza danych z naszej bazy wykazuje, że każdy film ma tylko jedną kategorię. Chcielibyśmy się pozbyć nadmiarowej relacji wiele do wielu i zastąpić ją relację jeden do wielu. Proszę przygotować migrację. Do tabeli `film` należy dołożyć kolumnę `category_id` wskazującą na kategorie z tabeli `category`. Należy też usunąć nadmiarową tabelę `film_category`. Migracja nie może zmieniać danych, które już są wprowadzone do tabeli - wszystkie dotychczasowe powiązania filmu z kategorią powinny być odwzorowane po migracji. Migrację proszę przygotować jako skrypt SQL i wkleić jego zawartość do formularza.
107 | Wskazówka: `ALTER TABLE`
108 | Wskazówka: https://stackoverflow.com/questions/21772631/sqlite-update-query-using-a-subquery
109 | Wskazówka: `DROP TABLE`
110 |
--------------------------------------------------------------------------------
/04_sqlite/praca_domowa/rozwiazanie/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.python.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | Flask = "*"
8 | gunicorn = "*"
9 | wtforms = "*"
10 | wtforms-json = "*"
11 |
12 | [dev-packages]
13 |
14 | [requires]
15 | python_version = "3.6"
16 |
--------------------------------------------------------------------------------
/04_sqlite/praca_domowa/rozwiazanie/Procfile:
--------------------------------------------------------------------------------
1 | web: gunicorn app:app
--------------------------------------------------------------------------------
/04_sqlite/praca_domowa/rozwiazanie/app.py:
--------------------------------------------------------------------------------
1 | import sqlite3
2 |
3 | from flask import Flask, g, request, jsonify
4 | from wtforms import Form, StringField, validators, IntegerField
5 |
6 | DATABASE = 'database.db'
7 |
8 | app = Flask(__name__)
9 |
10 |
11 | def get_db():
12 | db = getattr(g, '_database', None)
13 | if db is None:
14 | db = g.db = sqlite3.connect(DATABASE)
15 | # Foreign key support is disabled by default. One needs to set it up
16 | # once for each connection. More on this topic can be found here:
17 | # https://www.sqlite.org/foreignkeys.html (point 2.).
18 | db.execute('PRAGMA foreign_keys = 1')
19 | db.row_factory = sqlite3.Row
20 | return db
21 |
22 |
23 | @app.teardown_appcontext
24 | def close_connection(exception):
25 | db = getattr(g, '_database', None)
26 | if db is not None:
27 | db.close()
28 |
29 |
30 | class InvalidUsage(Exception):
31 | status_code = 400
32 |
33 | def __init__(self, error, status_code=None, payload=None):
34 | super().__init__(self)
35 | self.error = error
36 | if status_code is not None:
37 | self.status_code = status_code
38 | self.payload = payload
39 |
40 | def to_dict(self):
41 | rv = dict(self.payload or ())
42 | rv['error'] = self.error
43 | return rv
44 |
45 |
46 | @app.errorhandler(InvalidUsage)
47 | def handle_invalid_usage(error):
48 | response = jsonify(error.to_dict())
49 | response.status_code = error.status_code
50 | return response
51 |
52 |
53 | @app.route('/cities', methods=['GET', 'POST'])
54 | def cities():
55 | if request.method == 'GET':
56 | return get_cities()
57 | else:
58 | return post_new_city()
59 |
60 |
61 | # https://wtforms.readthedocs.io/en/stable/
62 | class CitiesValidationForm(Form):
63 | country_name = StringField(validators=[validators.optional()])
64 | per_page = IntegerField(validators=[validators.optional(),
65 | validators.number_range(min=1)])
66 | page = IntegerField(validators=[validators.optional(),
67 | validators.number_range(min=1)])
68 |
69 |
70 | def get_cities():
71 | db = get_db()
72 |
73 | form = CitiesValidationForm(request.args)
74 |
75 | if not form.validate():
76 | return jsonify(error=form.errors)
77 |
78 | per_page = form.data['per_page'] or -1
79 | limit = per_page
80 |
81 | page = form.data['page'] or 0
82 | page_index = page - 1
83 | offset = page_index * per_page
84 |
85 | if form.data['country_name']:
86 | cities_rows = db.execute(
87 | 'SELECT city.city FROM city '
88 | 'JOIN country ON city.country_id = country.country_id '
89 | 'WHERE country.country = ? COLLATE NOCASE '
90 | 'ORDER BY city.city COLLATE NOCASE '
91 | 'LIMIT ? OFFSET ?;',
92 | (form.data['country_name'], limit, offset)
93 | )
94 | else:
95 | cities_rows = db.execute(
96 | 'SELECT city FROM city '
97 | 'ORDER BY city COLLATE NOCASE '
98 | 'LIMIT ? OFFSET ?;',
99 | (limit, offset)
100 | )
101 | return jsonify([row[0] for row in cities_rows.fetchall()])
102 |
103 |
104 | def post_new_city():
105 | db = get_db()
106 |
107 | new_city = request.get_json()
108 | country_id = new_city.get('country_id')
109 | city_name = new_city.get('city_name')
110 |
111 | # Check if all required parameters are provided
112 | if country_id is None:
113 | raise InvalidUsage(f'missing "country_id" in request data')
114 | if city_name is None:
115 | raise InvalidUsage(f'missing "city_name" in request data')
116 |
117 | # # A -------------------------------------------------------------------- >
118 | # # Check if specified country exists.
119 | # country_id_exists = bool(
120 | # db.execute(
121 | # 'SELECT * FROM country '
122 | # 'WHERE country.country_id = ?;',
123 | # (country_id,)
124 | # ).fetchone()[0]
125 | # )
126 | # if not country_id_exists:
127 | # raise InvalidUsage(f'No country for country_id: "{country_id}"')
128 | #
129 | # # Check if specified city already exists in the country.
130 | # duplicate = bool(
131 | # db.execute(
132 | # 'SELECT count(*) FROM city '
133 | # 'WHERE city = :city_name '
134 | # 'AND country_id = :country_id;',
135 | # new_city
136 | # ).fetchone()[0]
137 | # )
138 | # if duplicate:
139 | # raise InvalidUsage(f'City called {city_name} already exists in the '
140 | # f'country with country_id = {country_id}')
141 | #
142 | # # === What could possibly go wrong here? ===
143 | #
144 | # # Now we can add our city.
145 | # db.execute(
146 | # 'INSERT INTO city (city, country_id) '
147 | # 'VALUES (:city_name, :country_id);',
148 | # new_city
149 | # )
150 | #
151 | # # At least we think we can...
152 | # db.commit()
153 | # # /A < -------------------------------------------------------------------
154 |
155 | # B -------------------------------------------------------------------- >
156 | try:
157 | db.execute(
158 | 'INSERT INTO city (city, country_id) '
159 | 'VALUES (:city_name, :country_id);',
160 | new_city
161 | )
162 | db.commit()
163 | except sqlite3.IntegrityError as error:
164 |
165 | db.rollback()
166 | error_reason = error.args[0]
167 |
168 | if error_reason.startswith('UNIQUE constraint failed'):
169 | raise InvalidUsage(f'City called {city_name} already exists in '
170 | f'a country with country_id = {country_id}')
171 |
172 | elif error_reason.startswith('FOREIGN KEY constraint failed'):
173 | raise InvalidUsage(f'No country with country_id = {country_id}')
174 |
175 | else:
176 | raise error
177 | # /B < -------------------------------------------------------------------
178 |
179 | db_city = db.execute(
180 | 'SELECT city_id, city as city_name, country_id FROM city '
181 | 'WHERE city.city = :city_name AND city.country_id = :country_id;',
182 | new_city
183 | ).fetchone()
184 |
185 | return jsonify(dict(db_city))
186 |
187 |
188 | @app.route('/lang_roles')
189 | def lang_roles():
190 | db = get_db()
191 | # Only INNER JOIN or LEFT JOIN sqlite.
192 | lang_roles_rows = db.execute(
193 | 'SELECT language.name, count(film_actor.film_id) '
194 | 'FROM language '
195 | 'LEFT JOIN film ON language.language_id = film.language_id '
196 | 'LEFT JOIN film_actor ON film.film_id = film_actor.film_id '
197 | 'GROUP BY language.name;'
198 | ).fetchall()
199 | return jsonify(dict(lang_roles_rows))
200 |
201 |
202 | if __name__ == '__main__':
203 | app.run(debug=True, use_debugger=False)
204 |
--------------------------------------------------------------------------------
/04_sqlite/praca_domowa/rozwiazanie/migracja.sql:
--------------------------------------------------------------------------------
1 | BEGIN TRANSACTION;
2 |
3 | ALTER TABLE film ADD COLUMN category_id SMALLINT REFERENCES category(category_id);
4 |
5 | UPDATE film SET category_id = (
6 | SELECT category_id
7 | FROM film_category
8 | WHERE film_category.film_id = film.film_id
9 | );
10 |
11 | DROP TABLE film_category;
12 |
13 | COMMIT;
14 |
--------------------------------------------------------------------------------
/04_sqlite/przyklady/Procfile:
--------------------------------------------------------------------------------
1 | web: gunicorn app:app
--------------------------------------------------------------------------------
/04_sqlite/przyklady/app.py:
--------------------------------------------------------------------------------
1 | from flask import (
2 | Flask,
3 | g,
4 | redirect,
5 | render_template,
6 | request,
7 | url_for,
8 | )
9 | import sqlite3
10 |
11 | app = Flask(__name__)
12 |
13 |
14 | DATABASE = 'database.db'
15 |
16 |
17 | def get_db():
18 | db = getattr(g, '_database', None)
19 | if db is None:
20 | db = g._database = sqlite3.connect(DATABASE)
21 | db.row_factory = sqlite3.Row
22 | return db
23 |
24 |
25 | @app.teardown_appcontext
26 | def close_connection(exception):
27 | db = getattr(g, '_database', None)
28 | if db is not None:
29 | db.close()
30 |
31 |
32 | @app.route('/films')
33 | def films_list():
34 | db = get_db()
35 | data = db.execute('SELECT title FROM film').fetchall()
36 | return render_template('films.html', films=data)
37 |
38 |
39 | @app.route('/films/')
40 | def single_film(film_id):
41 | db = get_db()
42 | data = db.execute(
43 | 'SELECT title, release_year, description FROM film WHERE film_id = :film_id',
44 | {'film_id': film_id}).fetchone()
45 | return render_template('single_film.html', film=data)
46 |
47 |
48 | @app.route('/films_with_category')
49 | def films_with_category_list():
50 | db = get_db()
51 | data = db.execute('''
52 | SELECT title, name from film
53 | JOIN film_category ON film.film_id = film_category.film_id
54 | JOIN category ON film_category.category_id = category.category_id;
55 | ''').fetchall()
56 | return render_template('films_with_category.html', films=data)
57 |
58 |
59 | @app.route('/films_with_category_order')
60 | def films_with_category_order_list():
61 | db = get_db()
62 | data = db.execute('''
63 | SELECT title, name from film
64 | JOIN film_category ON film.film_id = film_category.film_id
65 | JOIN category ON film_category.category_id = category.category_id
66 | order by category.name;
67 | ''').fetchall()
68 | return render_template('films_with_category.html', films=data)
69 |
70 |
71 | @app.route('/actors/add/sqli', methods=['GET', 'POST'])
72 | def add_actor_version_1():
73 | if request.method == 'POST':
74 | first_name = request.form['first_name']
75 | last_name = request.form['last_name']
76 | db = get_db()
77 | # DO NOT WRITE QUERIES LIKE THIS!!!11!1!
78 | db.executescript(
79 | 'INSERT INTO actor (first_name, last_name) VALUES ("{}", "{}")'
80 | .format(first_name, last_name)
81 | )
82 | db.commit()
83 | return redirect(url_for('actors_list'))
84 | else:
85 | return render_template('add_actor_sqli.html')
86 |
87 |
88 | @app.route('/actors/add', methods=['GET', 'POST'])
89 | def add_actor_version_2():
90 | if request.method == 'POST':
91 | first_name = request.form['first_name']
92 | last_name = request.form['last_name']
93 | db = get_db()
94 | db.execute(
95 | 'INSERT INTO actor (first_name, last_name) VALUES (?, ?)',
96 | (first_name, last_name)
97 | )
98 | db.commit()
99 | return redirect(url_for('actors_list'))
100 | else:
101 | return render_template('add_actor.html')
102 |
103 |
104 | @app.route('/actors/edit/', methods=['GET', 'POST'])
105 | def edit_actor(actor_id):
106 | if request.method == 'POST':
107 | first_name = request.form['first_name']
108 | last_name = request.form['last_name']
109 | db = get_db()
110 | db.execute(
111 | 'UPDATE actor SET first_name = ?, last_name = ? WHERE actor_id = ?',
112 | (first_name, last_name, actor_id)
113 | )
114 | db.commit()
115 | return redirect(url_for('actors_list'))
116 | else:
117 | db = get_db()
118 | actor = db.execute(
119 | 'SELECT first_name, last_name from actor WHERE actor_id = ?',
120 | (actor_id,)).fetchone()
121 | return render_template('edit_actor.html', actor=actor, actor_id=actor_id)
122 |
123 |
124 | @app.route('/actors')
125 | def actors_list():
126 | db = get_db()
127 | data = db.execute('SELECT first_name, last_name FROM actor ORDER BY last_update DESC').fetchall()
128 | return render_template('actors.html', actors=data)
129 |
130 |
131 | @app.route('/actors/delete/', methods=['GET', 'POST'])
132 | def delete_actor(actor_id):
133 | if request.method == 'POST':
134 | db = get_db()
135 | db.execute('DELETE FROM actor WHERE actor_id = ?', (actor_id,))
136 | db.commit()
137 | return redirect(url_for('actors_list'))
138 | else:
139 | db = get_db()
140 | actor = db.execute(
141 | 'SELECT first_name, last_name from actor WHERE actor_id = ?',
142 | (actor_id,)).fetchone()
143 | return render_template('delete_actor.html', actor=actor, actor_id=actor_id)
144 |
145 |
146 | @app.route('/actors_count')
147 | def actors_list_with_count():
148 | db = get_db()
149 | data = db.execute('SELECT first_name, last_name FROM actor ORDER BY last_update DESC').fetchall()
150 | count = db.execute('SELECT COUNT(*) FROM actor').fetchone()
151 | return render_template('actors_count.html', actors=data, count=count[0])
152 |
153 |
154 | @app.route('/')
155 | def main_view():
156 | return render_template('main.html')
157 |
158 |
159 | if __name__ == '__main__':
160 | app.run(debug=True)
161 |
--------------------------------------------------------------------------------
/04_sqlite/przyklady/prepare_db.py:
--------------------------------------------------------------------------------
1 | import sqlite3
2 |
3 | conn = sqlite3.connect('database.db')
4 | c = conn.cursor()
5 |
6 | with open('sqlite-sakila-schema.sql', 'r', encoding='utf-8') as create_file:
7 | create_query = create_file.read()
8 | with open('sqlite-sakila-insert-data.sql', 'r', encoding='utf-8') as insert_file:
9 | insert_query = insert_file.read()
10 |
11 | c.executescript(create_query)
12 | c.executescript(insert_query)
13 |
14 | conn.commit()
15 | conn.close()
16 |
--------------------------------------------------------------------------------
/04_sqlite/przyklady/requirements.txt:
--------------------------------------------------------------------------------
1 | click==6.7
2 | Flask==0.12.2
3 | gunicorn==19.7.1
4 | itsdangerous==0.24
5 | Jinja2==2.10
6 | MarkupSafe==1.0
7 | Werkzeug==0.14.1
8 |
--------------------------------------------------------------------------------
/04_sqlite/przyklady/templates/actors.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Aktorzy
6 |
7 |
8 |
9 | Imię i nazwisko
10 | {% for actor in actors %}
11 | - {{ actor['first_name'] + ' ' + actor['last_name'] }}
12 | {% endfor %}
13 |
14 |
15 |
--------------------------------------------------------------------------------
/04_sqlite/przyklady/templates/actors_count.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Aktorzy
6 |
7 |
8 | LICZBA AKTORÓW: {{ count }}
9 |
10 | Imię i nazwisko
11 | {% for actor in actors %}
12 | - {{ actor['first_name'] + ' ' + actor['last_name'] }}
13 | {% endfor %}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/04_sqlite/przyklady/templates/add_actor.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Dodaj aktora
6 |
7 |
8 |
15 |
16 |
--------------------------------------------------------------------------------
/04_sqlite/przyklady/templates/add_actor_sqli.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Dodaj aktora
6 |
7 |
8 |
15 |
16 |
--------------------------------------------------------------------------------
/04_sqlite/przyklady/templates/delete_actor.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Usuń aktora
6 |
7 |
8 | Imię: {{ actor['first_name'] }}
9 | Nazwisko: {{ actor['last_name'] }}
10 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/04_sqlite/przyklady/templates/edit_actor.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Edytuj aktora
6 |
7 |
8 |
15 |
16 |
19 |
20 |
--------------------------------------------------------------------------------
/04_sqlite/przyklady/templates/films.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Filmy
6 |
7 |
8 |
9 | Nazwa
10 | {% for film in films %}
11 | - {{ film['title'] }}
12 | {% endfor %}
13 |
14 |
15 |
--------------------------------------------------------------------------------
/04_sqlite/przyklady/templates/films_with_category.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Filmy z kategorią
6 |
7 |
8 |
9 | Nazwa, Kategoria
10 | {% for film in films %}
11 | - {{ film['title'] }} {{ film['name'] }}
12 | {% endfor %}
13 |
14 |
15 |
--------------------------------------------------------------------------------
/04_sqlite/przyklady/templates/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SELECT Python
6 |
7 |
8 |
18 |
19 |
--------------------------------------------------------------------------------
/04_sqlite/przyklady/templates/single_film.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Film {{ film['title'] }}
6 |
7 |
8 | {{ film['title'] }}
9 | Rok produkcji: {{ film['release_year'] }}
10 | Opis: {{ film['description'] }}
11 |
12 |
--------------------------------------------------------------------------------
/05_sqlalchemy/README.MD:
--------------------------------------------------------------------------------
1 | # Praca domowa nr 5
2 |
3 | Na rozwiązania czekamy do wtorku 10.04.2018 23:59:59 CEST.
4 |
5 | Rozwiązania prosimy przesyłać tylko za pośrednictwem formularza: https://goo.gl/forms/8ik0T9z3FeI8exzI2
6 |
7 | Prosimy o zadawanie pytań poprzez issue na GitHubie.
8 |
9 | ## Zadania
10 |
11 | 1. Rozwiązać podpunkty: 1, 2, 4, 5 pracy domowej z poprzednich zajęć wykorzystując bazę PostgreSQL na Heroku oraz SQLAlchemy.
12 | Interesuje nas tylko endpoint `/cities` i powiązanie z krajami.
13 | Pozostałe tabele możecie pomminąć w waszej bazie (Heroku nakłada limit na liczbę wierszy w darmowej bazie). Wasza baza powinna zawierać wszystkie maista i kraje wraz z powiązaniami między nimi, które są obecne w bazie sakila używanej na zajęciach.
14 | Aplikacja powinna działać z wieloma workerami.
15 | Ta aplikacja nie powinna zaburzać rozwiązania poprzedniego zadania (najlepiej stwórzcie osobną aplikację)
16 |
17 | 2. Rozwiązać zadanie ze ścieżką `/couter` z zajęć nr 1 wykorzystując Heroku PostgreSQL, SQLAlchemy i synchronizację na bazie.
18 |
19 | Dokumentacja dla zapytania `SELECT FOR UPDATE`:
20 | * https://www.postgresql.org/docs/current/static/sql-select.html (sekcja: *The Locking Clause*)
21 | * http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query.with_for_update
22 |
--------------------------------------------------------------------------------
/05_sqlalchemy/sqlalchemy.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {
6 | "slideshow": {
7 | "slide_type": "slide"
8 | }
9 | },
10 | "source": [
11 | "# SQL i kamień filozoficzny\n",
12 | "## SQLAlchemy\n",
13 | "\n",
14 | "### Marcin Jaroszewski\n",
15 | "### 05.IV.2018, Python Level UP"
16 | ]
17 | },
18 | {
19 | "cell_type": "markdown",
20 | "metadata": {
21 | "slideshow": {
22 | "slide_type": "subslide"
23 | }
24 | },
25 | "source": [
26 | ""
27 | ]
28 | },
29 | {
30 | "cell_type": "markdown",
31 | "metadata": {
32 | "slideshow": {
33 | "slide_type": "subslide"
34 | }
35 | },
36 | "source": [
37 | ""
38 | ]
39 | },
40 | {
41 | "cell_type": "markdown",
42 | "metadata": {
43 | "slideshow": {
44 | "slide_type": "slide"
45 | }
46 | },
47 | "source": [
48 | "# 1. ORM - Object-Relational Mapping"
49 | ]
50 | },
51 | {
52 | "cell_type": "markdown",
53 | "metadata": {
54 | "slideshow": {
55 | "slide_type": "subslide"
56 | }
57 | },
58 | "source": [
59 | "Sklejenie dwóch niezgodnych systemów typów za pomocą programowania obiektowego.\n",
60 | "\n",
61 | "\n",
62 | "Opis na wikipedii: https://en.wikipedia.org/wiki/Object-relational_mapping"
63 | ]
64 | },
65 | {
66 | "cell_type": "markdown",
67 | "metadata": {
68 | "slideshow": {
69 | "slide_type": "slide"
70 | }
71 | },
72 | "source": [
73 | "# 2. ORM w Python "
74 | ]
75 | },
76 | {
77 | "cell_type": "markdown",
78 | "metadata": {
79 | "slideshow": {
80 | "slide_type": "subslide"
81 | }
82 | },
83 | "source": [
84 | "- SQLALchemy: https://www.sqlalchemy.org/\n",
85 | "- PonyORM: https://ponyorm.com/\n",
86 | "- Django ORM: https://www.djangoproject.com/"
87 | ]
88 | },
89 | {
90 | "cell_type": "markdown",
91 | "metadata": {
92 | "slideshow": {
93 | "slide_type": "slide"
94 | }
95 | },
96 | "source": [
97 | "# 3. SQLAlchemy"
98 | ]
99 | },
100 | {
101 | "cell_type": "markdown",
102 | "metadata": {
103 | "slideshow": {
104 | "slide_type": "subslide"
105 | }
106 | },
107 | "source": [
108 | "- Duże.\n",
109 | "- Rozwijane przez wiele lat.\n",
110 | "- Powszechnie używane.\n",
111 | "- Nie zawsze łatwe w obsłudze."
112 | ]
113 | },
114 | {
115 | "cell_type": "markdown",
116 | "metadata": {
117 | "slideshow": {
118 | "slide_type": "subslide"
119 | }
120 | },
121 | "source": [
122 | "Będziemy się zajmować częścią zwaną **ORM**."
123 | ]
124 | },
125 | {
126 | "cell_type": "markdown",
127 | "metadata": {
128 | "slideshow": {
129 | "slide_type": "slide"
130 | }
131 | },
132 | "source": [
133 | "# 4. Instalacja"
134 | ]
135 | },
136 | {
137 | "cell_type": "markdown",
138 | "metadata": {
139 | "slideshow": {
140 | "slide_type": "subslide"
141 | }
142 | },
143 | "source": [
144 | "Będziemy używać SQLAlchemy razem z Flask więc proponuję zainstalować je w tym samym środowisku wirtualnym."
145 | ]
146 | },
147 | {
148 | "cell_type": "markdown",
149 | "metadata": {
150 | "slideshow": {
151 | "slide_type": "subslide"
152 | }
153 | },
154 | "source": [
155 | "```bash\n",
156 | "pip install SQLAlchemy\n",
157 | "```"
158 | ]
159 | },
160 | {
161 | "cell_type": "markdown",
162 | "metadata": {
163 | "slideshow": {
164 | "slide_type": "subslide"
165 | }
166 | },
167 | "source": [
168 | "instrukcja: http://docs.sqlalchemy.org/en/latest/intro.html#install-via-pip"
169 | ]
170 | },
171 | {
172 | "cell_type": "markdown",
173 | "metadata": {
174 | "slideshow": {
175 | "slide_type": "slide"
176 | }
177 | },
178 | "source": [
179 | "# 5. Modele i relacje"
180 | ]
181 | },
182 | {
183 | "cell_type": "markdown",
184 | "metadata": {
185 | "slideshow": {
186 | "slide_type": "subslide"
187 | }
188 | },
189 | "source": [
190 | "Mamy dwa przypadki pracy z SQLAlchemy:\n",
191 | "1. Nowy projekt, bazy jeszcze nie ma - od początku wdrażamy SQLAlchemy. \n",
192 | "W takim wypadku możemy schemat bazy danych wygenerować z modeli SQLAchemy.\n",
193 | "\n",
194 | "2. Już istniejący projekt - SQLAlchemy podłączamy do już istniejącej bazy danych. \n",
195 | "Modele (niepełne) możemy wygenerować \"automatycznie\" lub ręcznie (pełne)."
196 | ]
197 | },
198 | {
199 | "cell_type": "markdown",
200 | "metadata": {
201 | "slideshow": {
202 | "slide_type": "subslide"
203 | }
204 | },
205 | "source": [
206 | "W obu przypadkach lepiej dla nas i naszej aplikacji, aby modele były zgodne ze schematem bazy. W przeciwnym wypadku złe rzeczy mogą się stać."
207 | ]
208 | },
209 | {
210 | "cell_type": "markdown",
211 | "metadata": {
212 | "slideshow": {
213 | "slide_type": "fragment"
214 | }
215 | },
216 | "source": [
217 | "500 to łagodny wymiar kary."
218 | ]
219 | },
220 | {
221 | "cell_type": "markdown",
222 | "metadata": {
223 | "slideshow": {
224 | "slide_type": "subslide"
225 | }
226 | },
227 | "source": [
228 | "SQLAlchemy obiecuje nam dostęp do bazy danych i danych w niej zawartych w obiektowy i puyhonowy sposób. Zweryfikujemy te obietnice w praktyce łacząc się z bazą Sakila. "
229 | ]
230 | },
231 | {
232 | "cell_type": "markdown",
233 | "metadata": {
234 | "slideshow": {
235 | "slide_type": "subslide"
236 | }
237 | },
238 | "source": [
239 | "Pierwszy krok w wykorzystaniu SQLAlchemy wymaga zamodelowania bazy danych (schema)."
240 | ]
241 | },
242 | {
243 | "cell_type": "markdown",
244 | "metadata": {
245 | "slideshow": {
246 | "slide_type": "subslide"
247 | }
248 | },
249 | "source": [
250 | "Chcemy się połaczyć do istniejącej bazy wypełnionej danymi.\n",
251 | "\n",
252 | "Możemy do problemu podejść na dwa sposoby:\n",
253 | "1. Ręczny - sami napiszemy modele pasujące do istniejącej bazy.\n",
254 | "2. Automatyczny - coś wygeneruje modele za nas."
255 | ]
256 | },
257 | {
258 | "cell_type": "markdown",
259 | "metadata": {
260 | "slideshow": {
261 | "slide_type": "subslide"
262 | }
263 | },
264 | "source": [
265 | "Ja należę do ludzi leniwych i chciałbym, żeby komputer pracował za mnie więc zacznę od podejścia automatycznego.\n",
266 | "\n",
267 | "Dokumentacja: http://docs.sqlalchemy.org/en/latest/orm/extensions/automap.html\n",
268 | "Uwaga: http://docs.sqlalchemy.org/en/latest/core/reflection.html"
269 | ]
270 | },
271 | {
272 | "cell_type": "code",
273 | "execution_count": 1,
274 | "metadata": {
275 | "slideshow": {
276 | "slide_type": "subslide"
277 | }
278 | },
279 | "outputs": [
280 | {
281 | "name": "stdout",
282 | "output_type": "stream",
283 | "text": [
284 | "\n"
285 | ]
286 | }
287 | ],
288 | "source": [
289 | "from sqlalchemy.ext.automap import automap_base\n",
290 | "from sqlalchemy.orm import Session\n",
291 | "from sqlalchemy import create_engine\n",
292 | "\n",
293 | "Base = automap_base()\n",
294 | "\n",
295 | "engine = create_engine(\"sqlite:///sakila.db\")\n",
296 | "Base.prepare(engine, reflect=True)\n",
297 | "print(Base.classes.actor)"
298 | ]
299 | },
300 | {
301 | "cell_type": "code",
302 | "execution_count": 2,
303 | "metadata": {
304 | "slideshow": {
305 | "slide_type": "subslide"
306 | }
307 | },
308 | "outputs": [
309 | {
310 | "name": "stdout",
311 | "output_type": "stream",
312 | "text": [
313 | "Table('actor', MetaData(bind=None), Column('actor_id', INTEGER(), table=, primary_key=True, nullable=False), Column('first_name', VARCHAR(length=45), table=, nullable=False), Column('last_name', VARCHAR(length=45), table=, nullable=False), Column('last_update', TIMESTAMP(), table=), schema=None)\n"
314 | ]
315 | }
316 | ],
317 | "source": [
318 | "print(repr(Base.classes.actor.__table__))"
319 | ]
320 | },
321 | {
322 | "cell_type": "markdown",
323 | "metadata": {
324 | "slideshow": {
325 | "slide_type": "subslide"
326 | }
327 | },
328 | "source": [
329 | "Fajnie, pięknie, automat zadziałał."
330 | ]
331 | },
332 | {
333 | "cell_type": "markdown",
334 | "metadata": {
335 | "slideshow": {
336 | "slide_type": "subslide"
337 | }
338 | },
339 | "source": [
340 | "Ale my liczyliśmy na pełen kod modeli :(.\n",
341 | "\n",
342 | "Coś w styllu: https://pypi.python.org/pypi/sqlacodegen mogłoby nam pomóc.\n",
343 | "Jednak ten projekt ma ponad 2 lata, a jego repozytorium zostało skasowane :(."
344 | ]
345 | },
346 | {
347 | "cell_type": "markdown",
348 | "metadata": {
349 | "slideshow": {
350 | "slide_type": "subslide"
351 | }
352 | },
353 | "source": [
354 | "Do odważnych, świat należy."
355 | ]
356 | },
357 | {
358 | "cell_type": "markdown",
359 | "metadata": {
360 | "slideshow": {
361 | "slide_type": "subslide"
362 | }
363 | },
364 | "source": [
365 | "Spróbowałem `sqlacodegen` i \"zadziałało\". Coś zostało wygenerowane i jest dostępne w `auto_generated_sakila_models.py`. Zanim użyjemy automatycznie wygenerowanych modeli wypadałoby je sprawdzić (ręcznie?)."
366 | ]
367 | },
368 | {
369 | "cell_type": "markdown",
370 | "metadata": {
371 | "slideshow": {
372 | "slide_type": "subslide"
373 | }
374 | },
375 | "source": [
376 | "Interesują nas głównie modele `Actor` i `Film` wraz z powiązaniami."
377 | ]
378 | },
379 | {
380 | "cell_type": "code",
381 | "execution_count": 3,
382 | "metadata": {
383 | "slideshow": {
384 | "slide_type": "skip"
385 | }
386 | },
387 | "outputs": [],
388 | "source": [
389 | "# coding: utf-8\n",
390 | "from sqlalchemy import CheckConstraint, Column, DateTime, ForeignKey, Index, Integer, LargeBinary, Numeric, SmallInteger, String, Table, Text, text\n",
391 | "from sqlalchemy.orm import relationship\n",
392 | "from sqlalchemy.sql.sqltypes import NullType\n",
393 | "from sqlalchemy.ext.declarative import declarative_base\n",
394 | "\n",
395 | "\n",
396 | "Base = declarative_base()\n",
397 | "metadata = Base.metadata\n",
398 | "\n",
399 | "\n",
400 | "class Actor(Base):\n",
401 | " __tablename__ = 'actor'\n",
402 | "\n",
403 | " actor_id = Column(Integer, primary_key=True)\n",
404 | " first_name = Column(String(45), nullable=False)\n",
405 | " last_name = Column(String(45), nullable=False)\n",
406 | " last_update = Column(DateTime)\n",
407 | "\n",
408 | "\n",
409 | "class Addres(Base):\n",
410 | " __tablename__ = 'address'\n",
411 | "\n",
412 | " address_id = Column(Integer, primary_key=True)\n",
413 | " address = Column(String(50), nullable=False)\n",
414 | " address2 = Column(String(50), server_default=text(\"NULL\"))\n",
415 | " district = Column(String(20), nullable=False)\n",
416 | " city_id = Column(ForeignKey('city.city_id', ondelete='NO ACTION', onupdate='CASCADE'), nullable=False, index=True)\n",
417 | " postal_code = Column(String(10), server_default=text(\"NULL\"))\n",
418 | " phone = Column(String(20), nullable=False)\n",
419 | " last_update = Column(DateTime, nullable=False)\n",
420 | "\n",
421 | " city = relationship('City')\n",
422 | "\n",
423 | "\n",
424 | "class Category(Base):\n",
425 | " __tablename__ = 'category'\n",
426 | "\n",
427 | " category_id = Column(SmallInteger, primary_key=True)\n",
428 | " name = Column(String(25), nullable=False)\n",
429 | " last_update = Column(DateTime, nullable=False)\n",
430 | "\n",
431 | "\n",
432 | "class City(Base):\n",
433 | " __tablename__ = 'city'\n",
434 | "\n",
435 | " city_id = Column(Integer, primary_key=True)\n",
436 | " city = Column(String(50), nullable=False)\n",
437 | " country_id = Column(ForeignKey('country.country_id', ondelete='NO ACTION', onupdate='CASCADE'), nullable=False, index=True)\n",
438 | " last_update = Column(DateTime, nullable=False)\n",
439 | "\n",
440 | " country = relationship('Country')\n",
441 | "\n",
442 | " adresses = relationship('Addres')\n",
443 | "\n",
444 | "\n",
445 | "class Country(Base):\n",
446 | " __tablename__ = 'country'\n",
447 | "\n",
448 | " country_id = Column(SmallInteger, primary_key=True)\n",
449 | " country = Column(String(50), nullable=False)\n",
450 | " last_update = Column(DateTime)\n",
451 | "\n",
452 | "\n",
453 | "class Customer(Base):\n",
454 | " __tablename__ = 'customer'\n",
455 | "\n",
456 | " customer_id = Column(Integer, primary_key=True)\n",
457 | " store_id = Column(ForeignKey('store.store_id', ondelete='NO ACTION', onupdate='CASCADE'), nullable=False, index=True)\n",
458 | " first_name = Column(String(45), nullable=False)\n",
459 | " last_name = Column(String(45), nullable=False, index=True)\n",
460 | " email = Column(String(50), server_default=text(\"NULL\"))\n",
461 | " address_id = Column(ForeignKey('address.address_id', ondelete='NO ACTION', onupdate='CASCADE'), nullable=False, index=True)\n",
462 | " active = Column(String(1), nullable=False, server_default=text(\"'Y'\"))\n",
463 | " create_date = Column(DateTime, nullable=False)\n",
464 | " last_update = Column(DateTime, nullable=False)\n",
465 | "\n",
466 | " address = relationship('Addres')\n",
467 | " store = relationship('Store')\n",
468 | "\n",
469 | "\n",
470 | "t_customer_list = Table(\n",
471 | " 'customer_list', metadata,\n",
472 | " Column('ID', Integer),\n",
473 | " Column('name', NullType),\n",
474 | " Column('address', String(50)),\n",
475 | " Column('zip_code', String(10)),\n",
476 | " Column('phone', String(20)),\n",
477 | " Column('city', String(50)),\n",
478 | " Column('country', String(50)),\n",
479 | " Column('notes', NullType),\n",
480 | " Column('SID', Integer)\n",
481 | ")\n",
482 | "\n",
483 | "\n",
484 | "class Film(Base):\n",
485 | " __tablename__ = 'film'\n",
486 | " __table_args__ = (\n",
487 | " CheckConstraint(\"rating in ('G','PG','PG-13','R','NC-17')\"),\n",
488 | " )\n",
489 | "\n",
490 | " film_id = Column(Integer, primary_key=True)\n",
491 | " title = Column(String(255), nullable=False)\n",
492 | " description = Column(Text, server_default=text(\"NULL\"))\n",
493 | " release_year = Column(String(4), server_default=text(\"NULL\"))\n",
494 | " language_id = Column(ForeignKey('language.language_id'), nullable=False, index=True)\n",
495 | " original_language_id = Column(ForeignKey('language.language_id'), index=True, server_default=text(\"NULL\"))\n",
496 | " rental_duration = Column(SmallInteger, nullable=False, server_default=text(\"3\"))\n",
497 | " rental_rate = Column(Numeric(4, 2), nullable=False, server_default=text(\"4.99\"))\n",
498 | " length = Column(SmallInteger, server_default=text(\"NULL\"))\n",
499 | " replacement_cost = Column(Numeric(5, 2), nullable=False, server_default=text(\"19.99\"))\n",
500 | " rating = Column(String(10), server_default=text(\"'G'\"))\n",
501 | " special_features = Column(String(100), server_default=text(\"NULL\"))\n",
502 | " last_update = Column(DateTime, nullable=False)\n",
503 | "\n",
504 | " language = relationship('Language', primaryjoin='Film.language_id == Language.language_id')\n",
505 | " original_language = relationship('Language', primaryjoin='Film.original_language_id == Language.language_id')\n",
506 | "\n",
507 | "\n",
508 | "class FilmActor(Base):\n",
509 | " __tablename__ = 'film_actor'\n",
510 | "\n",
511 | " actor_id = Column(ForeignKey('actor.actor_id', ondelete='NO ACTION', onupdate='CASCADE'), primary_key=True, nullable=False, index=True)\n",
512 | " film_id = Column(ForeignKey('film.film_id', ondelete='NO ACTION', onupdate='CASCADE'), primary_key=True, nullable=False, index=True)\n",
513 | " last_update = Column(DateTime, nullable=False)\n",
514 | "\n",
515 | " actor = relationship('Actor')\n",
516 | " film = relationship('Film')\n",
517 | "\n",
518 | "\n",
519 | "class FilmCategory(Base):\n",
520 | " __tablename__ = 'film_category'\n",
521 | "\n",
522 | " film_id = Column(ForeignKey('film.film_id', ondelete='NO ACTION', onupdate='CASCADE'), primary_key=True, nullable=False, index=True)\n",
523 | " category_id = Column(ForeignKey('category.category_id', ondelete='NO ACTION', onupdate='CASCADE'), primary_key=True, nullable=False, index=True)\n",
524 | " last_update = Column(DateTime, nullable=False)\n",
525 | "\n",
526 | " category = relationship('Category')\n",
527 | " film = relationship('Film')\n",
528 | "\n",
529 | "\n",
530 | "t_film_list = Table(\n",
531 | " 'film_list', metadata,\n",
532 | " Column('FID', Integer),\n",
533 | " Column('title', String(255)),\n",
534 | " Column('description', Text),\n",
535 | " Column('category', String(25)),\n",
536 | " Column('price', Numeric(4, 2)),\n",
537 | " Column('length', SmallInteger),\n",
538 | " Column('rating', String(10)),\n",
539 | " Column('actors', NullType)\n",
540 | ")\n",
541 | "\n",
542 | "\n",
543 | "class FilmText(Base):\n",
544 | " __tablename__ = 'film_text'\n",
545 | "\n",
546 | " film_id = Column(SmallInteger, primary_key=True)\n",
547 | " title = Column(String(255), nullable=False)\n",
548 | " description = Column(Text)\n",
549 | "\n",
550 | "\n",
551 | "class Inventory(Base):\n",
552 | " __tablename__ = 'inventory'\n",
553 | " __table_args__ = (\n",
554 | " Index('idx_fk_film_id_store_id', 'store_id', 'film_id'),\n",
555 | " )\n",
556 | "\n",
557 | " inventory_id = Column(Integer, primary_key=True)\n",
558 | " film_id = Column(ForeignKey('film.film_id', ondelete='NO ACTION', onupdate='CASCADE'), nullable=False, index=True)\n",
559 | " store_id = Column(ForeignKey('store.store_id', ondelete='NO ACTION', onupdate='CASCADE'), nullable=False)\n",
560 | " last_update = Column(DateTime, nullable=False)\n",
561 | "\n",
562 | " film = relationship('Film')\n",
563 | " store = relationship('Store')\n",
564 | "\n",
565 | "\n",
566 | "class Language(Base):\n",
567 | " __tablename__ = 'language'\n",
568 | "\n",
569 | " language_id = Column(SmallInteger, primary_key=True)\n",
570 | " name = Column(String(20), nullable=False)\n",
571 | " last_update = Column(DateTime, nullable=False)\n",
572 | "\n",
573 | "\n",
574 | "class Payment(Base):\n",
575 | " __tablename__ = 'payment'\n",
576 | "\n",
577 | " payment_id = Column(Integer, primary_key=True)\n",
578 | " customer_id = Column(ForeignKey('customer.customer_id'), nullable=False, index=True)\n",
579 | " staff_id = Column(ForeignKey('staff.staff_id'), nullable=False, index=True)\n",
580 | " rental_id = Column(ForeignKey('rental.rental_id', ondelete='SET NULL', onupdate='CASCADE'), server_default=text(\"NULL\"))\n",
581 | " amount = Column(Numeric(5, 2), nullable=False)\n",
582 | " payment_date = Column(DateTime, nullable=False)\n",
583 | " last_update = Column(DateTime, nullable=False)\n",
584 | "\n",
585 | " customer = relationship('Customer')\n",
586 | " rental = relationship('Rental')\n",
587 | " staff = relationship('Staff')\n",
588 | "\n",
589 | "\n",
590 | "class Rental(Base):\n",
591 | " __tablename__ = 'rental'\n",
592 | " __table_args__ = (\n",
593 | " Index('idx_rental_uq', 'rental_date', 'inventory_id', 'customer_id', unique=True),\n",
594 | " )\n",
595 | "\n",
596 | " rental_id = Column(Integer, primary_key=True)\n",
597 | " rental_date = Column(DateTime, nullable=False)\n",
598 | " inventory_id = Column(ForeignKey('inventory.inventory_id'), nullable=False, index=True)\n",
599 | " customer_id = Column(ForeignKey('customer.customer_id'), nullable=False, index=True)\n",
600 | " return_date = Column(DateTime, server_default=text(\"NULL\"))\n",
601 | " staff_id = Column(ForeignKey('staff.staff_id'), nullable=False, index=True)\n",
602 | " last_update = Column(DateTime, nullable=False)\n",
603 | "\n",
604 | " customer = relationship('Customer')\n",
605 | " inventory = relationship('Inventory')\n",
606 | " staff = relationship('Staff')\n",
607 | "\n",
608 | "\n",
609 | "t_sales_by_film_category = Table(\n",
610 | " 'sales_by_film_category', metadata,\n",
611 | " Column('category', String(25)),\n",
612 | " Column('total_sales', NullType)\n",
613 | ")\n",
614 | "\n",
615 | "\n",
616 | "t_sales_by_store = Table(\n",
617 | " 'sales_by_store', metadata,\n",
618 | " Column('store_id', Integer),\n",
619 | " Column('store', NullType),\n",
620 | " Column('manager', NullType),\n",
621 | " Column('total_sales', NullType)\n",
622 | ")\n",
623 | "\n",
624 | "\n",
625 | "t_sqlite_sequence = Table(\n",
626 | " 'sqlite_sequence', metadata,\n",
627 | " Column('name', NullType),\n",
628 | " Column('seq', NullType)\n",
629 | ")\n",
630 | "\n",
631 | "\n",
632 | "class Staff(Base):\n",
633 | " __tablename__ = 'staff'\n",
634 | "\n",
635 | " staff_id = Column(SmallInteger, primary_key=True)\n",
636 | " first_name = Column(String(45), nullable=False)\n",
637 | " last_name = Column(String(45), nullable=False)\n",
638 | " address_id = Column(ForeignKey('address.address_id', ondelete='NO ACTION', onupdate='CASCADE'), nullable=False, index=True)\n",
639 | " picture = Column(LargeBinary, server_default=text(\"NULL\"))\n",
640 | " email = Column(String(50), server_default=text(\"NULL\"))\n",
641 | " store_id = Column(ForeignKey('store.store_id', ondelete='NO ACTION', onupdate='CASCADE'), nullable=False, index=True)\n",
642 | " active = Column(SmallInteger, nullable=False, server_default=text(\"1\"))\n",
643 | " username = Column(String(16), nullable=False)\n",
644 | " password = Column(String(40), server_default=text(\"NULL\"))\n",
645 | " last_update = Column(DateTime, nullable=False)\n",
646 | "\n",
647 | " address = relationship('Addres')\n",
648 | " store = relationship('Store', primaryjoin='Staff.store_id == Store.store_id')\n",
649 | "\n",
650 | "\n",
651 | "t_staff_list = Table(\n",
652 | " 'staff_list', metadata,\n",
653 | " Column('ID', SmallInteger),\n",
654 | " Column('name', NullType),\n",
655 | " Column('address', String(50)),\n",
656 | " Column('zip_code', String(10)),\n",
657 | " Column('phone', String(20)),\n",
658 | " Column('city', String(50)),\n",
659 | " Column('country', String(50)),\n",
660 | " Column('SID', Integer)\n",
661 | ")\n",
662 | "\n",
663 | "\n",
664 | "class Store(Base):\n",
665 | " __tablename__ = 'store'\n",
666 | "\n",
667 | " store_id = Column(Integer, primary_key=True)\n",
668 | " manager_staff_id = Column(ForeignKey('staff.staff_id'), nullable=False, index=True)\n",
669 | " address_id = Column(ForeignKey('address.address_id'), nullable=False, index=True)\n",
670 | " last_update = Column(DateTime, nullable=False)\n",
671 | "\n",
672 | " address = relationship('Addres')\n",
673 | " manager_staff = relationship('Staff', primaryjoin='Store.manager_staff_id == Staff.staff_id')\n"
674 | ]
675 | },
676 | {
677 | "cell_type": "markdown",
678 | "metadata": {
679 | "slideshow": {
680 | "slide_type": "subslide"
681 | }
682 | },
683 | "source": [
684 | "## Relacje"
685 | ]
686 | },
687 | {
688 | "cell_type": "markdown",
689 | "metadata": {
690 | "slideshow": {
691 | "slide_type": "subslide"
692 | }
693 | },
694 | "source": [
695 | "* `relationship`: http://docs.sqlalchemy.org/en/latest/orm/relationships.html\n",
696 | "* `backref`: http://docs.sqlalchemy.org/en/latest/orm/backref.html\n",
697 | "* Jak się dostać do obiektów powiązanych.\n",
698 | "* Czy odwołania do obiektów powiązanych kosztują?"
699 | ]
700 | },
701 | {
702 | "cell_type": "markdown",
703 | "metadata": {
704 | "slideshow": {
705 | "slide_type": "subslide"
706 | }
707 | },
708 | "source": [
709 | "## jeden-do-jeden"
710 | ]
711 | },
712 | {
713 | "cell_type": "markdown",
714 | "metadata": {
715 | "slideshow": {
716 | "slide_type": "subslide"
717 | }
718 | },
719 | "source": [
720 | "http://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html#one-to-one"
721 | ]
722 | },
723 | {
724 | "cell_type": "markdown",
725 | "metadata": {
726 | "slideshow": {
727 | "slide_type": "subslide"
728 | }
729 | },
730 | "source": [
731 | "## jeden-do-wielu"
732 | ]
733 | },
734 | {
735 | "cell_type": "markdown",
736 | "metadata": {
737 | "slideshow": {
738 | "slide_type": "subslide"
739 | }
740 | },
741 | "source": [
742 | "http://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html#one-to-many"
743 | ]
744 | },
745 | {
746 | "cell_type": "markdown",
747 | "metadata": {
748 | "slideshow": {
749 | "slide_type": "subslide"
750 | }
751 | },
752 | "source": [
753 | "## wiele-do-wielu"
754 | ]
755 | },
756 | {
757 | "cell_type": "markdown",
758 | "metadata": {
759 | "slideshow": {
760 | "slide_type": "subslide"
761 | }
762 | },
763 | "source": [
764 | "http://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html#many-to-many"
765 | ]
766 | },
767 | {
768 | "cell_type": "markdown",
769 | "metadata": {
770 | "slideshow": {
771 | "slide_type": "notes"
772 | }
773 | },
774 | "source": [
775 | "Pokazać relację `Country` - `City`"
776 | ]
777 | },
778 | {
779 | "cell_type": "markdown",
780 | "metadata": {
781 | "slideshow": {
782 | "slide_type": "subslide"
783 | }
784 | },
785 | "source": [
786 | "# 6. Querowanie"
787 | ]
788 | },
789 | {
790 | "cell_type": "markdown",
791 | "metadata": {
792 | "slideshow": {
793 | "slide_type": "subslide"
794 | }
795 | },
796 | "source": [
797 | "Czyli pobieranie danych z bazy."
798 | ]
799 | },
800 | {
801 | "cell_type": "markdown",
802 | "metadata": {
803 | "slideshow": {
804 | "slide_type": "subslide"
805 | }
806 | },
807 | "source": [
808 | "Musimy mieć jakieś \"połączenie\" do bazy danych."
809 | ]
810 | },
811 | {
812 | "cell_type": "markdown",
813 | "metadata": {
814 | "slideshow": {
815 | "slide_type": "fragment"
816 | }
817 | },
818 | "source": [
819 | "Potrzebujemuy sesji: http://docs.sqlalchemy.org/en/latest/orm/session.html"
820 | ]
821 | },
822 | {
823 | "cell_type": "code",
824 | "execution_count": 4,
825 | "metadata": {
826 | "slideshow": {
827 | "slide_type": "subslide"
828 | }
829 | },
830 | "outputs": [],
831 | "source": [
832 | "from sqlalchemy.orm import sessionmaker\n",
833 | "\n",
834 | "engine = create_engine('sqlite:///sakila.db')\n",
835 | "Session = sessionmaker(bind=engine)\n",
836 | "session = Session()"
837 | ]
838 | },
839 | {
840 | "cell_type": "code",
841 | "execution_count": 5,
842 | "metadata": {
843 | "slideshow": {
844 | "slide_type": "subslide"
845 | }
846 | },
847 | "outputs": [],
848 | "source": [
849 | "# Pobranie wszystkich aktorów z bazy\n",
850 | "all_actors = session.query(Actor)"
851 | ]
852 | },
853 | {
854 | "cell_type": "code",
855 | "execution_count": 6,
856 | "metadata": {
857 | "slideshow": {
858 | "slide_type": "subslide"
859 | }
860 | },
861 | "outputs": [
862 | {
863 | "name": "stdout",
864 | "output_type": "stream",
865 | "text": [
866 | "SELECT actor.actor_id AS actor_actor_id, actor.first_name AS actor_first_name, actor.last_name AS actor_last_name, actor.last_update AS actor_last_update \n",
867 | "FROM actor\n"
868 | ]
869 | }
870 | ],
871 | "source": [
872 | "print(all_actors)"
873 | ]
874 | },
875 | {
876 | "cell_type": "code",
877 | "execution_count": 7,
878 | "metadata": {
879 | "slideshow": {
880 | "slide_type": "subslide"
881 | }
882 | },
883 | "outputs": [
884 | {
885 | "name": "stdout",
886 | "output_type": "stream",
887 | "text": [
888 | "\n"
889 | ]
890 | }
891 | ],
892 | "source": [
893 | "print(type(all_actors))"
894 | ]
895 | },
896 | {
897 | "cell_type": "markdown",
898 | "metadata": {
899 | "slideshow": {
900 | "slide_type": "subslide"
901 | }
902 | },
903 | "source": [
904 | "Coś \"nie pykło\"."
905 | ]
906 | },
907 | {
908 | "cell_type": "markdown",
909 | "metadata": {
910 | "slideshow": {
911 | "slide_type": "subslide"
912 | }
913 | },
914 | "source": [
915 | "* Czym jest `Query`: http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query"
916 | ]
917 | },
918 | {
919 | "cell_type": "code",
920 | "execution_count": 8,
921 | "metadata": {
922 | "slideshow": {
923 | "slide_type": "subslide"
924 | }
925 | },
926 | "outputs": [
927 | {
928 | "name": "stdout",
929 | "output_type": "stream",
930 | "text": [
931 | "139872189543704\n",
932 | "139871952354608\n",
933 | "SELECT actor.actor_id AS actor_actor_id, actor.first_name AS actor_first_name, actor.last_name AS actor_last_name, actor.last_update AS actor_last_update \n",
934 | "FROM actor \n",
935 | "WHERE actor.first_name = ?\n"
936 | ]
937 | }
938 | ],
939 | "source": [
940 | "# eksperymentujemy dalej\n",
941 | "all_penelopes = all_actors.filter(Actor.first_name == 'PENELOPE')\n",
942 | "print(id(all_actors))\n",
943 | "print(id(all_penelopes))\n",
944 | "print(all_penelopes)"
945 | ]
946 | },
947 | {
948 | "cell_type": "code",
949 | "execution_count": 9,
950 | "metadata": {
951 | "slideshow": {
952 | "slide_type": "subslide"
953 | }
954 | },
955 | "outputs": [
956 | {
957 | "name": "stdout",
958 | "output_type": "stream",
959 | "text": [
960 | "PENELOPE GUINESS\n",
961 | "PENELOPE PINKETT\n",
962 | "PENELOPE CRONYN\n",
963 | "PENELOPE MONROE\n"
964 | ]
965 | }
966 | ],
967 | "source": [
968 | "for penelope in all_penelopes:\n",
969 | " print('{} {}'.format(penelope.first_name, penelope.last_name))"
970 | ]
971 | },
972 | {
973 | "cell_type": "code",
974 | "execution_count": 10,
975 | "metadata": {
976 | "slideshow": {
977 | "slide_type": "subslide"
978 | }
979 | },
980 | "outputs": [
981 | {
982 | "name": "stdout",
983 | "output_type": "stream",
984 | "text": [
985 | "[<__main__.Actor object at 0x7f367a08bd68>, <__main__.Actor object at 0x7f367a08bf98>, <__main__.Actor object at 0x7f367a08b908>, <__main__.Actor object at 0x7f367a09cba8>]\n"
986 | ]
987 | }
988 | ],
989 | "source": [
990 | "print(all_penelopes.all())"
991 | ]
992 | },
993 | {
994 | "cell_type": "markdown",
995 | "metadata": {
996 | "slideshow": {
997 | "slide_type": "slide"
998 | }
999 | },
1000 | "source": [
1001 | "# 7. Wkładanie danych do bazy"
1002 | ]
1003 | },
1004 | {
1005 | "cell_type": "code",
1006 | "execution_count": 11,
1007 | "metadata": {
1008 | "slideshow": {
1009 | "slide_type": "subslide"
1010 | }
1011 | },
1012 | "outputs": [],
1013 | "source": [
1014 | "marcin = Actor(\n",
1015 | " first_name='Marcin',\n",
1016 | " last_name='Jaroszewski'\n",
1017 | ")"
1018 | ]
1019 | },
1020 | {
1021 | "cell_type": "code",
1022 | "execution_count": 12,
1023 | "metadata": {
1024 | "slideshow": {
1025 | "slide_type": "subslide"
1026 | }
1027 | },
1028 | "outputs": [
1029 | {
1030 | "name": "stdout",
1031 | "output_type": "stream",
1032 | "text": [
1033 | "<__main__.Actor object at 0x7f367a09ce10>\n"
1034 | ]
1035 | }
1036 | ],
1037 | "source": [
1038 | "marcin_q = session.query(Actor).filter(Actor.last_name == 'Jaroszewski').first()\n",
1039 | "print(marcin_q)"
1040 | ]
1041 | },
1042 | {
1043 | "cell_type": "markdown",
1044 | "metadata": {
1045 | "slideshow": {
1046 | "slide_type": "subslide"
1047 | }
1048 | },
1049 | "source": [
1050 | "Ja chcę robić karierę na srebnym ekranie!\n",
1051 | "\n",
1052 | "Dlaczego nie ma mnie w bazie aktorów?"
1053 | ]
1054 | },
1055 | {
1056 | "cell_type": "code",
1057 | "execution_count": 13,
1058 | "metadata": {
1059 | "slideshow": {
1060 | "slide_type": "subslide"
1061 | }
1062 | },
1063 | "outputs": [
1064 | {
1065 | "name": "stdout",
1066 | "output_type": "stream",
1067 | "text": [
1068 | "207 Marcin Jaroszewski None\n"
1069 | ]
1070 | }
1071 | ],
1072 | "source": [
1073 | "marcin = Actor(\n",
1074 | " first_name='Marcin',\n",
1075 | " last_name='Jaroszewski'\n",
1076 | ")\n",
1077 | "session.add(marcin)\n",
1078 | "session.commit()\n",
1079 | "print(marcin.actor_id, marcin.first_name, marcin.last_name, marcin.last_update)"
1080 | ]
1081 | },
1082 | {
1083 | "cell_type": "code",
1084 | "execution_count": 14,
1085 | "metadata": {
1086 | "slideshow": {
1087 | "slide_type": "subslide"
1088 | }
1089 | },
1090 | "outputs": [
1091 | {
1092 | "name": "stdout",
1093 | "output_type": "stream",
1094 | "text": [
1095 | "<__main__.Actor object at 0x7f367a09ce10>\n",
1096 | "206 MARCIN Jaroszewski None\n"
1097 | ]
1098 | }
1099 | ],
1100 | "source": [
1101 | "marcin_q = session.query(Actor).filter(Actor.last_name == 'Jaroszewski').first()\n",
1102 | "print(marcin_q)\n",
1103 | "print(marcin_q.actor_id, marcin_q.first_name, marcin_q.last_name, marcin_q.last_update)"
1104 | ]
1105 | },
1106 | {
1107 | "cell_type": "markdown",
1108 | "metadata": {
1109 | "slideshow": {
1110 | "slide_type": "slide"
1111 | }
1112 | },
1113 | "source": [
1114 | "# 8. Zmienianie danych w bazie"
1115 | ]
1116 | },
1117 | {
1118 | "cell_type": "code",
1119 | "execution_count": 15,
1120 | "metadata": {
1121 | "slideshow": {
1122 | "slide_type": "subslide"
1123 | }
1124 | },
1125 | "outputs": [],
1126 | "source": [
1127 | "marcin_q.first_name = 'MARCIN'\n",
1128 | "session.commit()"
1129 | ]
1130 | },
1131 | {
1132 | "cell_type": "code",
1133 | "execution_count": 16,
1134 | "metadata": {
1135 | "slideshow": {
1136 | "slide_type": "subslide"
1137 | }
1138 | },
1139 | "outputs": [
1140 | {
1141 | "name": "stdout",
1142 | "output_type": "stream",
1143 | "text": [
1144 | "<__main__.Actor object at 0x7f367a09ce10>\n",
1145 | "206 MARCIN Jaroszewski None\n"
1146 | ]
1147 | }
1148 | ],
1149 | "source": [
1150 | "marcin_q = session.query(Actor).filter(Actor.last_name == 'Jaroszewski').first()\n",
1151 | "print(marcin_q)\n",
1152 | "print(marcin_q.actor_id, marcin_q.first_name, marcin_q.last_name, marcin_q.last_update)"
1153 | ]
1154 | },
1155 | {
1156 | "cell_type": "markdown",
1157 | "metadata": {
1158 | "slideshow": {
1159 | "slide_type": "slide"
1160 | }
1161 | },
1162 | "source": [
1163 | "# Pytania"
1164 | ]
1165 | },
1166 | {
1167 | "cell_type": "markdown",
1168 | "metadata": {
1169 | "slideshow": {
1170 | "slide_type": "subslide"
1171 | }
1172 | },
1173 | "source": [
1174 | "## Co sprawia, że zapytanie zostanie wykonane?"
1175 | ]
1176 | },
1177 | {
1178 | "cell_type": "markdown",
1179 | "metadata": {
1180 | "slideshow": {
1181 | "slide_type": "fragment"
1182 | }
1183 | },
1184 | "source": [
1185 | "* `all()`: http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query.all\n",
1186 | "* `first()`: http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query.first\n",
1187 | "* `one()`: http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query.one\n",
1188 | "* `one_or_none`: http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query.one_or_none\n",
1189 | "* `scalar`: http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query.scalar\n",
1190 | "* iterowanie po wynikach"
1191 | ]
1192 | },
1193 | {
1194 | "cell_type": "markdown",
1195 | "metadata": {
1196 | "slideshow": {
1197 | "slide_type": "subslide"
1198 | }
1199 | },
1200 | "source": [
1201 | "## Czym jest transakcja?"
1202 | ]
1203 | },
1204 | {
1205 | "cell_type": "markdown",
1206 | "metadata": {
1207 | "slideshow": {
1208 | "slide_type": "fragment"
1209 | }
1210 | },
1211 | "source": [
1212 | "http://docs.sqlalchemy.org/en/latest/core/connections.html#sqlalchemy.engine.Transaction"
1213 | ]
1214 | },
1215 | {
1216 | "cell_type": "markdown",
1217 | "metadata": {
1218 | "slideshow": {
1219 | "slide_type": "subslide"
1220 | }
1221 | },
1222 | "source": [
1223 | "## Co robi flush?"
1224 | ]
1225 | },
1226 | {
1227 | "cell_type": "markdown",
1228 | "metadata": {
1229 | "slideshow": {
1230 | "slide_type": "fragment"
1231 | }
1232 | },
1233 | "source": [
1234 | "http://docs.sqlalchemy.org/en/latest/orm/session_api.html#sqlalchemy.orm.session.Session.flush"
1235 | ]
1236 | },
1237 | {
1238 | "cell_type": "markdown",
1239 | "metadata": {
1240 | "slideshow": {
1241 | "slide_type": "subslide"
1242 | }
1243 | },
1244 | "source": [
1245 | "## Co to commit?"
1246 | ]
1247 | },
1248 | {
1249 | "cell_type": "markdown",
1250 | "metadata": {
1251 | "slideshow": {
1252 | "slide_type": "fragment"
1253 | }
1254 | },
1255 | "source": [
1256 | "http://docs.sqlalchemy.org/en/latest/orm/session_api.html#sqlalchemy.orm.session.Session.commit"
1257 | ]
1258 | },
1259 | {
1260 | "cell_type": "markdown",
1261 | "metadata": {
1262 | "slideshow": {
1263 | "slide_type": "subslide"
1264 | }
1265 | },
1266 | "source": [
1267 | "## Co to rollback?"
1268 | ]
1269 | },
1270 | {
1271 | "cell_type": "markdown",
1272 | "metadata": {
1273 | "slideshow": {
1274 | "slide_type": "fragment"
1275 | }
1276 | },
1277 | "source": [
1278 | "http://docs.sqlalchemy.org/en/latest/orm/session_api.html#sqlalchemy.orm.session.Session.rollback"
1279 | ]
1280 | },
1281 | {
1282 | "cell_type": "markdown",
1283 | "metadata": {
1284 | "slideshow": {
1285 | "slide_type": "subslide"
1286 | }
1287 | },
1288 | "source": [
1289 | "## Jak się bronić przed \"popsutymi\" transakcjami?"
1290 | ]
1291 | },
1292 | {
1293 | "cell_type": "markdown",
1294 | "metadata": {
1295 | "slideshow": {
1296 | "slide_type": "fragment"
1297 | }
1298 | },
1299 | "source": [
1300 | "* `try ... except ...`\n",
1301 | "* `contextmanager`"
1302 | ]
1303 | },
1304 | {
1305 | "cell_type": "markdown",
1306 | "metadata": {
1307 | "slideshow": {
1308 | "slide_type": "slide"
1309 | }
1310 | },
1311 | "source": [
1312 | "# 9. SQLAlchemy i Flask"
1313 | ]
1314 | },
1315 | {
1316 | "cell_type": "markdown",
1317 | "metadata": {
1318 | "slideshow": {
1319 | "slide_type": "subslide"
1320 | }
1321 | },
1322 | "source": [
1323 | "Flask ma wtyczkę usprawniającą działanie z SQLAlchemy: http://flask-sqlalchemy.pocoo.org\n",
1324 | "\n",
1325 | "Ale nie będziemy jej dziś używać - nie chcę zaciemniać obrazu."
1326 | ]
1327 | },
1328 | {
1329 | "cell_type": "markdown",
1330 | "metadata": {
1331 | "slideshow": {
1332 | "slide_type": "slide"
1333 | }
1334 | },
1335 | "source": [
1336 | "# 10. Jak to w życiu bywa"
1337 | ]
1338 | }
1339 | ],
1340 | "metadata": {
1341 | "celltoolbar": "Slideshow",
1342 | "kernelspec": {
1343 | "display_name": "Python 3",
1344 | "language": "python",
1345 | "name": "python3"
1346 | },
1347 | "language_info": {
1348 | "codemirror_mode": {
1349 | "name": "ipython",
1350 | "version": 3
1351 | },
1352 | "file_extension": ".py",
1353 | "mimetype": "text/x-python",
1354 | "name": "python",
1355 | "nbconvert_exporter": "python",
1356 | "pygments_lexer": "ipython3",
1357 | "version": "3.6.5"
1358 | }
1359 | },
1360 | "nbformat": 4,
1361 | "nbformat_minor": 2
1362 | }
1363 |
--------------------------------------------------------------------------------
/06_asyncio/asyncio.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {
6 | "slideshow": {
7 | "slide_type": "slide"
8 | }
9 | },
10 | "source": [
11 | "# W pętli zdarzeń - Asyncio\n",
12 | "\n",
13 | "### Marcin Markiewicz\n",
14 | "### 12.IV.2018, Python Level UP"
15 | ]
16 | },
17 | {
18 | "cell_type": "markdown",
19 | "metadata": {
20 | "slideshow": {
21 | "slide_type": "subslide"
22 | }
23 | },
24 | "source": [
25 | ""
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "metadata": {
31 | "slideshow": {
32 | "slide_type": "subslide"
33 | }
34 | },
35 | "source": [
36 | ""
37 | ]
38 | },
39 | {
40 | "cell_type": "markdown",
41 | "metadata": {
42 | "slideshow": {
43 | "slide_type": "slide"
44 | }
45 | },
46 | "source": [
47 | "# Programowanie asyncio - po co to komu?"
48 | ]
49 | },
50 | {
51 | "cell_type": "markdown",
52 | "metadata": {
53 | "slideshow": {
54 | "slide_type": "subslide"
55 | }
56 | },
57 | "source": [
58 | "## Pytanie: jeśli teraz modne jest programowanie asynchroniczne, to co robiliśmy do tej pory?\n",
59 | "\n",
60 | "Trzeba sobie zdać sprawę z tego, że procesory są bardzo szybkie. Dane wewnątrz struktur procesora przetwarzają się o rząd wielkości szybciej niż w pamięci ram. Dostęp do świata zewnętrzenego, dyski (nawet ssd), sieć, to są kolejne rzędy wielkości wolniej. W związku z tym, w programach, które komunikują się z siecią, procesor głównie czeka (idle time). "
61 | ]
62 | },
63 | {
64 | "cell_type": "markdown",
65 | "metadata": {
66 | "slideshow": {
67 | "slide_type": "subslide"
68 | }
69 | },
70 | "source": [
71 | "## Jak odzyskać stracony czas\n",
72 | "\n",
73 | "Stracony czas (oczekiwanie na synchronizację danych) można odzyskać. Możemy użyć \"klasycznych\" rozwiązań, czyli wielowątkowości lub wieloprocesowosći. Jednak te technologie wiążą się ze stosunkowo dużym kosztem przełączania procesów (switching context). Problem ten wzrasta wraz z obciążeniem systemu. Przy dużej ilości procesów, zaczyna być kosztowny, a także może stanowić główny czas pracy programu. "
74 | ]
75 | },
76 | {
77 | "cell_type": "markdown",
78 | "metadata": {
79 | "slideshow": {
80 | "slide_type": "subslide"
81 | }
82 | },
83 | "source": [
84 | "Rozwiązaniem problemu przełączania jest eliminacja przełączania procesów. Ideą programowania asynchronicznego jest cykliczne wywoływanie procedur w pętli zdarzeń. W jednym procesie, bez narzutu związanego z przełączaniem procesów."
85 | ]
86 | },
87 | {
88 | "cell_type": "markdown",
89 | "metadata": {
90 | "slideshow": {
91 | "slide_type": "subslide"
92 | }
93 | },
94 | "source": [
95 | "Asynchroniczność jest wspierana bezpośrednio przez kernel linuxowy. AIO (Asynchronous I/O) jest to api pozawalające na operacje wyjścia/wejścia bez blokowania na czas oczekiwania na dane. Zamiast tego, proces może kontynuować pracę, i sprawdzić za jakiś czas czy są dostępne wyniki operacji. \n",
96 | "\n",
97 | "Link dla zainteresowanych:\n",
98 | "- https://oxnz.github.io/2016/10/13/linux-aio/\n",
99 | "- https://www.ibm.com/developerworks/linux/library/l-async/"
100 | ]
101 | },
102 | {
103 | "cell_type": "markdown",
104 | "metadata": {
105 | "slideshow": {
106 | "slide_type": "slide"
107 | }
108 | },
109 | "source": [
110 | "# Jak to robi Python"
111 | ]
112 | },
113 | {
114 | "cell_type": "markdown",
115 | "metadata": {
116 | "slideshow": {
117 | "slide_type": "subslide"
118 | }
119 | },
120 | "source": [
121 | "## Asyncio\n",
122 | "\n",
123 | "Moduł asyncio jest implementacją asynchronicznych operacji wyjścia/wejścia. Oryginalnie moduł ten był zaimplementowany poprzez wykorzystanie generatorów (`yield` i `yield from`). Obecnie (Python 3.5+) są dedykowane słowa kluczowe takie jak: `await` i `async`. "
124 | ]
125 | },
126 | {
127 | "cell_type": "markdown",
128 | "metadata": {
129 | "slideshow": {
130 | "slide_type": "subslide"
131 | }
132 | },
133 | "source": [
134 | "## W pętli zdarzeń\n",
135 | "\n",
136 | "EventLoop jest podstawowym konceptem asynchronicznego programowania. Jest to pętla zdarzeń. Czyli cyklicznie wywoływanych `korutyn`. Pomysł ten przypomina spotkanie tematyczne grupy ludzi, w którym może odzywać się tylko ta osoba, która ma `token`. Przkazanie tokenu następuje tylko w miejscach umiesczenia `await` w kodzie programu. Jest to zgoła inna koncepcja od programowania klasycznych wątków/procesów, gdzie przełączanie następuje w dowolnym miejscu kodu pythonowego, programista nie wie gdzie ono nastąpi.\n"
137 | ]
138 | },
139 | {
140 | "cell_type": "markdown",
141 | "metadata": {
142 | "slideshow": {
143 | "slide_type": "slide"
144 | }
145 | },
146 | "source": [
147 | "## Przykład"
148 | ]
149 | },
150 | {
151 | "cell_type": "markdown",
152 | "metadata": {
153 | "slideshow": {
154 | "slide_type": "subslide"
155 | }
156 | },
157 | "source": [
158 | "Zróbmy AB test pobierania danych z serwera http. Użyjemy strony http://httpbin.org. Dla pełnego efektu potrzebujemy odpowiedzi serwera, która jest stosunkowo długa. http://httpbin.org/delay/1 generuje odpowiedź serwera która jest generowana przez około 1s "
159 | ]
160 | },
161 | {
162 | "cell_type": "markdown",
163 | "metadata": {
164 | "slideshow": {
165 | "slide_type": "subslide"
166 | }
167 | },
168 | "source": [
169 | "## Klasyczny request\n",
170 | "\n",
171 | "```python\n",
172 | "import requests\n",
173 | "import time\n",
174 | "\n",
175 | "def make_sync_requests(max, url):\n",
176 | " print('Sync requests')\n",
177 | " start_time = time.time()\n",
178 | "\n",
179 | " response = []\n",
180 | " for i in range(0, max):\n",
181 | " requests.get(url)\n",
182 | " response.append(i)\n",
183 | "\n",
184 | " print(response)\n",
185 | " execution_time = time.time() - start_time\n",
186 | " print(f'execution time: {execution_time}s')\n",
187 | "```\n"
188 | ]
189 | },
190 | {
191 | "cell_type": "markdown",
192 | "metadata": {
193 | "slideshow": {
194 | "slide_type": "subslide"
195 | }
196 | },
197 | "source": [
198 | "## Asynchroniczny request\n",
199 | "\n",
200 | "```python\n",
201 | "import asyncio\n",
202 | "import time\n",
203 | "\n",
204 | "from aiohttp import ClientSession\n",
205 | "\n",
206 | "async def make_asyncio_request(url, index, session):\n",
207 | " async with session.get(url) as response:\n",
208 | " return index\n",
209 | "\n",
210 | "async def make_asyncio_requests(max, url):\n",
211 | " print('Asyncio requests')\n",
212 | " start_time = time.time()\n",
213 | " async with ClientSession() as session:\n",
214 | " tasks = [make_asyncio_request(url, index, session) for index in range(0, max)]\n",
215 | " responses = await asyncio.gather(*tasks)\n",
216 | " print(responses)\n",
217 | " execution_time = time.time() - start_time\n",
218 | " print(f'execution time: {execution_time}s')\n",
219 | "```"
220 | ]
221 | },
222 | {
223 | "cell_type": "markdown",
224 | "metadata": {
225 | "slideshow": {
226 | "slide_type": "subslide"
227 | }
228 | },
229 | "source": [
230 | "# Przykład użycia asyncpg\n",
231 | "\n",
232 | "```python\n",
233 | "import asyncio\n",
234 | "import asyncpg\n",
235 | "import datetime\n",
236 | "\n",
237 | "async def main():\n",
238 | " # Establish a connection to an existing database named \"test\"\n",
239 | " # as a \"postgres\" user.\n",
240 | " conn = await asyncpg.connect('postgresql://postgres@localhost/test')\n",
241 | " # Execute a statement to create a new table.\n",
242 | " await conn.execute('''CREATE TABLE users(id serial PRIMARY KEY,name text,dob date)''')\n",
243 | " # Insert a record into the created table.\n",
244 | " await conn.execute('''INSERT INTO users(name, dob) VALUES($1, $2)''', 'Bob', datetime.date(1984, 3, 1))\n",
245 | " # Select a row from the table.\n",
246 | " row = await conn.fetchrow('SELECT * FROM users WHERE name = $1', 'Bob')\n",
247 | " # *row* now contains\n",
248 | " # asyncpg.Record(id=1, name='Bob', dob=datetime.date(1984, 3, 1))\n",
249 | " # Close the connection.\n",
250 | " await conn.close()\n",
251 | " \n",
252 | "asyncio.get_event_loop().run_until_complete(main())\n",
253 | "```\n"
254 | ]
255 | },
256 | {
257 | "cell_type": "markdown",
258 | "metadata": {
259 | "slideshow": {
260 | "slide_type": "slide"
261 | }
262 | },
263 | "source": [
264 | "# Od czego zacząć przygodę z asyncio"
265 | ]
266 | },
267 | {
268 | "cell_type": "markdown",
269 | "metadata": {
270 | "slideshow": {
271 | "slide_type": "subslide"
272 | }
273 | },
274 | "source": [
275 | "## Jak uruchomić korutynę\n",
276 | "\n",
277 | "```python\n",
278 | "import asyncio\n",
279 | "\n",
280 | "async def main():\n",
281 | " await asyncio.sleep(10)\n",
282 | "\n",
283 | "asyncio.get_event_loop().run_until_complete(main()) \n",
284 | "```"
285 | ]
286 | },
287 | {
288 | "cell_type": "markdown",
289 | "metadata": {
290 | "slideshow": {
291 | "slide_type": "subslide"
292 | }
293 | },
294 | "source": [
295 | "## Uruchomienie wielu korutyn i czekanie na ich rezultaty\n",
296 | "\n",
297 | "```python\n",
298 | "import asyncio\n",
299 | "\n",
300 | "async def important_task(index):\n",
301 | " await asyncio.sleep(10)\n",
302 | " return index\n",
303 | "\n",
304 | "async def run():\n",
305 | " tasks = [\n",
306 | " important_task(index)\n",
307 | " for index in range(0, 10)\n",
308 | " ]\n",
309 | " print(await asyncio.gather(*tasks))\n",
310 | "\n",
311 | "asyncio.get_event_loop().run_until_complete(run())\n",
312 | "```\n",
313 | "**Results:**\n",
314 | "```python\n",
315 | "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n",
316 | "```\n"
317 | ]
318 | },
319 | {
320 | "cell_type": "markdown",
321 | "metadata": {
322 | "slideshow": {
323 | "slide_type": "subslide"
324 | }
325 | },
326 | "source": [
327 | "## Uruchomienie wielu korutyn i wykorzystanie ich rezultatów jak tylko to możliwe\n",
328 | "\n",
329 | "\n",
330 | "```python\n",
331 | "import asyncio\n",
332 | "\n",
333 | "async def important_task(index):\n",
334 | " await asyncio.sleep(index)\n",
335 | " return index\n",
336 | "\n",
337 | "async def run():\n",
338 | " tasks = [important_task(index) for index in range(10, 0, -1)]\n",
339 | " results = []\n",
340 | " for future in asyncio.as_completed(tasks):\n",
341 | " result = await future\n",
342 | " results.append(result)\n",
343 | " print(results)\n",
344 | "\n",
345 | "asyncio.get_event_loop().run_until_complete(run())\n",
346 | "```\n",
347 | "\n",
348 | "**Results:**\n",
349 | "```python\n",
350 | "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n",
351 | "```\n",
352 | "\n"
353 | ]
354 | },
355 | {
356 | "cell_type": "markdown",
357 | "metadata": {
358 | "slideshow": {
359 | "slide_type": "subslide"
360 | }
361 | },
362 | "source": [
363 | "# Rekursywne tworzenie tasków\n",
364 | "\n",
365 | "```python\n",
366 | "import asyncio\n",
367 | "\n",
368 | "async def important_task(index):\n",
369 | " await asyncio.sleep(1)\n",
370 | " print(index)\n",
371 | "\n",
372 | "async def run():\n",
373 | " loop = asyncio.get_event_loop()\n",
374 | " for index in range(10):\n",
375 | " loop.create_task(important_task(index))\n",
376 | "\n",
377 | "asyncio.get_event_loop().run_until_complete(run())\n",
378 | "```\n",
379 | "\n",
380 | "**Results:**\n",
381 | "```python\n",
382 | "0\n",
383 | "1\n",
384 | "2\n",
385 | "3\n",
386 | "4\n",
387 | "5\n",
388 | "6\n",
389 | "7\n",
390 | "8\n",
391 | "9\n",
392 | "```"
393 | ]
394 | },
395 | {
396 | "cell_type": "markdown",
397 | "metadata": {
398 | "slideshow": {
399 | "slide_type": "subslide"
400 | }
401 | },
402 | "source": [
403 | "## Nieskończona pętla z obsługą sygnałów systemowych\n",
404 | "\n",
405 | "```python\n",
406 | "loop = self.get_event_loop()\n",
407 | "\n",
408 | "# setup services, make db connection etc...\n",
409 | "async def bootstrap():\n",
410 | " pass\n",
411 | "\n",
412 | "# terminate connection to db, redis etc...\n",
413 | "async def shutdown():\n",
414 | " pass\n",
415 | "\n",
416 | "# stop the loop, cancel asyncio task etc...\n",
417 | "def terminate():\n",
418 | " loop.stop()\n",
419 | "\n",
420 | "# Initial events\n",
421 | "loop.create_task(bootstrap())\n",
422 | "\n",
423 | "# Register signals for graceful termination\n",
424 | "for _signal in (signal.SIGINT, signal.SIGTERM):\n",
425 | " loop.add_signal_handler(_signal, terminate())\n",
426 | " \n",
427 | "# Main loop start\n",
428 | "try:\n",
429 | " loop.run_forever()\n",
430 | "finally:\n",
431 | " loop.run_until_complete(shutdown())\n",
432 | " loop.close()\n",
433 | "```"
434 | ]
435 | },
436 | {
437 | "cell_type": "markdown",
438 | "metadata": {
439 | "slideshow": {
440 | "slide_type": "subslide"
441 | }
442 | },
443 | "source": [
444 | "## Mój task jest długi, i blokuje przełączanie pętli\n",
445 | "\n",
446 | "Rozwiązaniem jest podział taska na mniejsze kawałki, które będziemy wyowłaywać przez await lub jako osobne taski.\n",
447 | "Mozna także użyć sztuczki: `await asyncio.sleep(0)`\n",
448 | "\n",
449 | "```python\n",
450 | "async def long_task():\n",
451 | " for strategy in strategies:\n",
452 | " strategy.make_decision()\n",
453 | " await.sleep(0) \n",
454 | "```"
455 | ]
456 | },
457 | {
458 | "cell_type": "markdown",
459 | "metadata": {
460 | "slideshow": {
461 | "slide_type": "slide"
462 | }
463 | },
464 | "source": [
465 | "# Co dalej?\n"
466 | ]
467 | },
468 | {
469 | "cell_type": "markdown",
470 | "metadata": {
471 | "slideshow": {
472 | "slide_type": "subslide"
473 | }
474 | },
475 | "source": [
476 | "## Przydatne linki\n",
477 | "1. https://docs.python.org/3/library/asyncio.html\n",
478 | "1. https://hackernoon.com/asyncio-for-the-working-python-developer-5c468e6e2e8e\n",
479 | "1. https://medium.com/python-pandemonium/asyncio-coroutine-patterns-beyond-await-a6121486656f\n",
480 | "1. https://medium.com/@yeraydiazdiaz/asyncio-coroutine-patterns-errors-and-cancellation-3bb422e961ff\n",
481 | "1. https://magic.io/blog/asyncpg-1m-rows-from-postgres-to-python/\n",
482 | "1. https://magic.io/blog/uvloop-blazing-fast-python-networking/\n",
483 | "1. https://github.com/channelcat/sanic\n",
484 | "1. http://lucumr.pocoo.org/2016/10/30/i-dont-understand-asyncio/"
485 | ]
486 | },
487 | {
488 | "cell_type": "markdown",
489 | "metadata": {
490 | "slideshow": {
491 | "slide_type": "subslide"
492 | }
493 | },
494 | "source": [
495 | "## Włącz tryb debug\n",
496 | "\n",
497 | "Możesz ustawić zmienną środowiskową `PYTHONASYNCIODEBUG=1` lub dodać w kodzie wywołanie `loop.set_debug()`."
498 | ]
499 | },
500 | {
501 | "cell_type": "markdown",
502 | "metadata": {
503 | "slideshow": {
504 | "slide_type": "subslide"
505 | }
506 | },
507 | "source": [
508 | "## Asyncio ma przyzwoity domyślny logger\n",
509 | "\n",
510 | "Należy tylko pamiętać o ustawieniu poziomu debug.\n",
511 | "\n",
512 | "Przykładowa prosta konfiguracja\n",
513 | "\n",
514 | "```python\n",
515 | "import logging\n",
516 | "formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')\n",
517 | "chandler = logging.StreamHandler()\n",
518 | "chandler.setFormatter(formatter)\n",
519 | "logger = logging.getLogger()\n",
520 | "logger.setLevel(logging.DEBUG)\n",
521 | "logger.addHandler(chandler)\n",
522 | "```"
523 | ]
524 | }
525 | ],
526 | "metadata": {
527 | "celltoolbar": "Slideshow",
528 | "kernelspec": {
529 | "display_name": "Python 3",
530 | "language": "python",
531 | "name": "python3"
532 | },
533 | "language_info": {
534 | "codemirror_mode": {
535 | "name": "ipython",
536 | "version": 3
537 | },
538 | "file_extension": ".py",
539 | "mimetype": "text/x-python",
540 | "name": "python",
541 | "nbconvert_exporter": "python",
542 | "pygments_lexer": "ipython3",
543 | "version": "3.6.3"
544 | }
545 | },
546 | "nbformat": 4,
547 | "nbformat_minor": 2
548 | }
549 |
--------------------------------------------------------------------------------
/06_asyncio/asyncio_request_demo.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import time
3 | import requests
4 |
5 | from aiohttp import ClientSession
6 |
7 |
8 | async def make_asyncio_request(url, index, session):
9 | async with session.get(url) as response:
10 | return index
11 |
12 |
13 | async def make_asyncio_requests(max, url):
14 | print('Asyncio requests')
15 | start_time = time.time()
16 |
17 | async with ClientSession() as session:
18 | tasks = [
19 | make_asyncio_request(url, index, session)
20 | for index in range(0, max)
21 | ]
22 | responses = await asyncio.gather(*tasks)
23 |
24 | print(responses)
25 | execution_time = time.time() - start_time
26 | print(f'execution time: {execution_time}s')
27 |
28 |
29 | def make_sync_requests(max, url):
30 | print('Sync requests')
31 | start_time = time.time()
32 |
33 | response = []
34 | for i in range(0, max):
35 | requests.get(url)
36 | response.append(i)
37 |
38 | print(response)
39 | execution_time = time.time() - start_time
40 | print(f'execution time: {execution_time}s')
41 |
42 |
43 | # Test it!
44 |
45 | max = 10
46 | url = 'http://httpbin.org/delay/1'
47 |
48 | print('=========================================')
49 |
50 | loop = asyncio.get_event_loop()
51 | loop.run_until_complete(make_asyncio_requests(max, url))
52 |
53 | print('-----------------------------------------')
54 | make_sync_requests(max, url)
55 |
56 | print('-----------------------------------------')
57 |
--------------------------------------------------------------------------------
/07_celery/celery.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {
6 | "slideshow": {
7 | "slide_type": "slide"
8 | }
9 | },
10 | "source": [
11 | "# Sprytny sposób na prokrastynację\n",
12 | "## Praca z asynchroniczną kolejką zadań Celery\n",
13 | "### Jakub Szponder\n",
14 | "#### Python Level Up, 19.04.2018"
15 | ]
16 | },
17 | {
18 | "cell_type": "markdown",
19 | "metadata": {
20 | "slideshow": {
21 | "slide_type": "subslide"
22 | }
23 | },
24 | "source": [
25 | ""
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "metadata": {
31 | "slideshow": {
32 | "slide_type": "subslide"
33 | }
34 | },
35 | "source": [
36 | ""
37 | ]
38 | },
39 | {
40 | "cell_type": "markdown",
41 | "metadata": {
42 | "slideshow": {
43 | "slide_type": "subslide"
44 | }
45 | },
46 | "source": [
47 | "# http://praktyki.daftcode.pl/\n",
48 | "Czekamy do 20 kwietnia!"
49 | ]
50 | },
51 | {
52 | "cell_type": "markdown",
53 | "metadata": {
54 | "slideshow": {
55 | "slide_type": "slide"
56 | }
57 | },
58 | "source": [
59 | "# Aplikacja webowa\n",
60 | ""
61 | ]
62 | },
63 | {
64 | "cell_type": "markdown",
65 | "metadata": {
66 | "slideshow": {
67 | "slide_type": "subslide"
68 | }
69 | },
70 | "source": [
71 | "W aplikacjach webowych zależy nam na skracaniu czasu obsługi requestu, dzięki temu:\n",
72 | "- użytkownicy nas bardziej lubią\n",
73 | "- nie blokujemy workerów aplikacyjnych, przez co jesteśmy w stanie obsługiwać więcej requestów"
74 | ]
75 | },
76 | {
77 | "cell_type": "markdown",
78 | "metadata": {
79 | "slideshow": {
80 | "slide_type": "subslide"
81 | }
82 | },
83 | "source": [
84 | "# Aplikacja webowa wykorzystująca kolejkę tasków\n",
85 | ""
86 | ]
87 | },
88 | {
89 | "cell_type": "markdown",
90 | "metadata": {
91 | "slideshow": {
92 | "slide_type": "subslide"
93 | }
94 | },
95 | "source": [
96 | "Serwer zleca czasochłonne zadania (__Taski__) do wykonania __Workerowi__, czyli procesowi, którego zadaniem jest wykonywanie tego typu zadań.\n",
97 | "Do komunikacji pomiędzy __Aplikacją__ a __Workerem__ używamy __Brokera__"
98 | ]
99 | },
100 | {
101 | "cell_type": "markdown",
102 | "metadata": {
103 | "slideshow": {
104 | "slide_type": "subslide"
105 | }
106 | },
107 | "source": [
108 | "Co może być dobrym __Taskiem__?\n",
109 | "- zadanie, które możemy odłożyć na później (nie musi być zrobione w trakcie zwracania Responsu)\n",
110 | "- czasochłonne zadanie, np. wysyłanie requestu na zewnętrzny serwer"
111 | ]
112 | },
113 | {
114 | "cell_type": "markdown",
115 | "metadata": {
116 | "slideshow": {
117 | "slide_type": "subslide"
118 | }
119 | },
120 | "source": [
121 | "# Przykłady z życia\n",
122 | "- wysyłka maila po stworzeniu konta użytkownikowi\n",
123 | "- generowanie raportu w PDFie\n",
124 | "- import/eksport dużych plików\n",
125 | "- serwis do skalowania obrazów"
126 | ]
127 | },
128 | {
129 | "cell_type": "markdown",
130 | "metadata": {
131 | "slideshow": {
132 | "slide_type": "subslide"
133 | }
134 | },
135 | "source": [
136 | "# Trochę większy przykład z życia - Twitter"
137 | ]
138 | },
139 | {
140 | "cell_type": "markdown",
141 | "metadata": {
142 | "slideshow": {
143 | "slide_type": "subslide"
144 | }
145 | },
146 | "source": [
147 | ""
148 | ]
149 | },
150 | {
151 | "cell_type": "markdown",
152 | "metadata": {
153 | "slideshow": {
154 | "slide_type": "slide"
155 | }
156 | },
157 | "source": [
158 | "# Celery\n",
159 | "- najpopularniejsza kolejka tasków w Pythonie\n",
160 | "- minimalny przykład jest bardzo krótki i łatwy do stworzenia\n",
161 | "- pod spodem dzieje się trochę magii"
162 | ]
163 | },
164 | {
165 | "cell_type": "markdown",
166 | "metadata": {
167 | "slideshow": {
168 | "slide_type": "slide"
169 | }
170 | },
171 | "source": [
172 | "# Architektura\n",
173 | "- __Klient__\n",
174 | "- __Broker__\n",
175 | "- __Worker__"
176 | ]
177 | },
178 | {
179 | "cell_type": "markdown",
180 | "metadata": {
181 | "slideshow": {
182 | "slide_type": "subslide"
183 | }
184 | },
185 | "source": [
186 | "## Klient\n",
187 | "Aplikacja, która korzysta wykorzystuje asynchroniczne zadania"
188 | ]
189 | },
190 | {
191 | "cell_type": "markdown",
192 | "metadata": {
193 | "slideshow": {
194 | "slide_type": "subslide"
195 | }
196 | },
197 | "source": [
198 | "## Broker\n",
199 | "- Kolejka na której odkładane są zadania do wykonania\n",
200 | "- Celery umożliwia wybranie jednego z wielu typów brokerów\n",
201 | "- Narzędzia, które mogą być brokerem:\n",
202 | " * __Redis__\n",
203 | " * RabbitMQ\n",
204 | " * zwykła relacyjna baza danych (SQLite / PostgreSQL) \n",
205 | "- argumenty przekazywane do taska są serializowane (domyślnie do JSONa) i zapisywane przy pomocy brokera razem z taskiem "
206 | ]
207 | },
208 | {
209 | "cell_type": "markdown",
210 | "metadata": {
211 | "slideshow": {
212 | "slide_type": "subslide"
213 | }
214 | },
215 | "source": [
216 | "## Worker\n",
217 | "Proces, który pobiera zadania z kolejki, a następnie je wykonuje"
218 | ]
219 | },
220 | {
221 | "cell_type": "markdown",
222 | "metadata": {
223 | "slideshow": {
224 | "slide_type": "slide"
225 | }
226 | },
227 | "source": [
228 | "# Redis\n",
229 | "- struktura danych, która umożliwia trzymanie danych w pamięci operacyjnej\n",
230 | "- używane do cachowania, jako broker do kolejek zadań, a także jako baza danych\n",
231 | "- trzyma dane w postaci klucz-wartość\n",
232 | "- jest interaktywny tutorial online: http://try.redis.io/\n",
233 | "- a tutaj informacje o instalowaniu: https://redis.io/topics/quickstart"
234 | ]
235 | },
236 | {
237 | "cell_type": "markdown",
238 | "metadata": {
239 | "slideshow": {
240 | "slide_type": "slide"
241 | }
242 | },
243 | "source": [
244 | "## Instalacja Celery\n",
245 | "```bash\n",
246 | "pip install celery\n",
247 | "pip install -U \"celery[redis]\"\n",
248 | "```"
249 | ]
250 | },
251 | {
252 | "cell_type": "markdown",
253 | "metadata": {
254 | "slideshow": {
255 | "slide_type": "slide"
256 | }
257 | },
258 | "source": [
259 | "# Pierwszy kod!\n",
260 | "1. Tworzymy aplikację Celery i definiujemy taska\n",
261 | "2. Uruchamiamy Redisa\n",
262 | "3. Startujemy Workera\n",
263 | "4. Kolejkujemy taska"
264 | ]
265 | },
266 | {
267 | "cell_type": "markdown",
268 | "metadata": {
269 | "slideshow": {
270 | "slide_type": "subslide"
271 | }
272 | },
273 | "source": [
274 | "## Definicja aplikacji Celery\n",
275 | "```python\n",
276 | "from celery import Celery\n",
277 | "\n",
278 | "app = Celery('tasks', broker='redis://localhost')\n",
279 | "\n",
280 | "@app.task\n",
281 | "def add(x, y):\n",
282 | " return x + y\n",
283 | "```"
284 | ]
285 | },
286 | {
287 | "cell_type": "markdown",
288 | "metadata": {
289 | "slideshow": {
290 | "slide_type": "subslide"
291 | }
292 | },
293 | "source": [
294 | "## Starowanie Workera\n",
295 | "```bash\n",
296 | "celery -A tasks worker --loglevel=info\n",
297 | "```"
298 | ]
299 | },
300 | {
301 | "cell_type": "markdown",
302 | "metadata": {
303 | "slideshow": {
304 | "slide_type": "subslide"
305 | }
306 | },
307 | "source": [
308 | "## Uruchamianie funkcji taska\n",
309 | "- w dalszym ciągu można normalnie wywoływać funkcję taska\n",
310 | "```python\n",
311 | "add(3, 5)\n",
312 | "```\n",
313 | "- a tak się wywołuje taska z wykorzystaniem Celery\n",
314 | "```python\n",
315 | "add.delay(3, 5)\n",
316 | "```"
317 | ]
318 | },
319 | {
320 | "cell_type": "markdown",
321 | "metadata": {
322 | "slideshow": {
323 | "slide_type": "subslide"
324 | }
325 | },
326 | "source": [
327 | "Wynik wywołania taska za pomocą delay to obiekt klasy __AsyncResult__\n",
328 | "http://docs.celeryproject.org/en/latest/reference/celery.result.html#celery.result.AsyncResult"
329 | ]
330 | },
331 | {
332 | "cell_type": "markdown",
333 | "metadata": {
334 | "slideshow": {
335 | "slide_type": "subslide"
336 | }
337 | },
338 | "source": [
339 | "- __delay__ to \"skrót\", który pod spodem wywołuje bardziej potężną metodę __apply_async__\n",
340 | "- __apply_async__ pozwala nam np. określić za ile sekund chcemy uruchomić taska (_countdown_)\n",
341 | "```python\n",
342 | "add.apply_async(args=(3, 5), countdown=10)\n",
343 | "```\n",
344 | "- więcej: http://docs.celeryproject.org/en/latest/userguide/calling.html#basics"
345 | ]
346 | },
347 | {
348 | "cell_type": "markdown",
349 | "metadata": {
350 | "slideshow": {
351 | "slide_type": "slide"
352 | }
353 | },
354 | "source": [
355 | "# Używanie Celery z aplikacji we Flasku\n",
356 | "```python\n",
357 | "from flask import Flask\n",
358 | "from celery import Celery\n",
359 | "\n",
360 | "app = Flask(__name__)\n",
361 | "app.config['CELERY_BROKER_URL'] = 'redis://localhost'\n",
362 | "\n",
363 | "celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'])\n",
364 | "celery.conf.update(app.config)\n",
365 | "\n",
366 | "@celery.task()\n",
367 | "def add(x, y):\n",
368 | " return x + y\n",
369 | "```"
370 | ]
371 | },
372 | {
373 | "cell_type": "markdown",
374 | "metadata": {
375 | "slideshow": {
376 | "slide_type": "slide"
377 | }
378 | },
379 | "source": [
380 | "# Przykład 1\n",
381 | "Stworzyć endpointy:\n",
382 | "- `/sync_request`, który wykona synchronicznie GET na adres `http://httpbin.org/delay/5`\n",
383 | "- `/async_request`, który wykona request pod ten sam adres, ale z wykorzystaniem __Celery__"
384 | ]
385 | },
386 | {
387 | "cell_type": "code",
388 | "execution_count": null,
389 | "metadata": {
390 | "slideshow": {
391 | "slide_type": "subslide"
392 | }
393 | },
394 | "outputs": [],
395 | "source": [
396 | "import requests\n",
397 | "\n",
398 | "@celery.task()\n",
399 | "def make_request():\n",
400 | " requests.get('http://httpbin.org/delay/5')\n",
401 | "\n",
402 | "\n",
403 | "@app.route('/sync_request')\n",
404 | "def make_sync_request():\n",
405 | " start = time.time()\n",
406 | " make_request() # bez delay\n",
407 | " end = time.time()\n",
408 | " return str(end - start)\n",
409 | "\n",
410 | " \n",
411 | "@app.route('/async_request')\n",
412 | "def make_async_request():\n",
413 | " start = time.time()\n",
414 | " make_request.delay() # z delay\n",
415 | " end = time.time()\n",
416 | " return str(end - start)"
417 | ]
418 | },
419 | {
420 | "cell_type": "markdown",
421 | "metadata": {
422 | "slideshow": {
423 | "slide_type": "subslide"
424 | }
425 | },
426 | "source": [
427 | "## Uruchamianie\n",
428 | "\n",
429 | "```bash\n",
430 | "celery -A app.celery worker --loglevel=info\n",
431 | "```"
432 | ]
433 | },
434 | {
435 | "cell_type": "markdown",
436 | "metadata": {
437 | "slideshow": {
438 | "slide_type": "slide"
439 | }
440 | },
441 | "source": [
442 | "# Przykład 2\n",
443 | "Obsłużyć endpoint `/users`\n",
444 | "- metoda `GET` - wyświetla formularz tworzenia użytkownika z jednym polem - `email`, po submicie idzie `POST` na `/users/`\n",
445 | "- metoda `POST` - wysyła powitalnego maila z tematem `Python Level Up`, o treści `Welcome to Python Level Up Sample Website!` na podany adres"
446 | ]
447 | },
448 | {
449 | "cell_type": "code",
450 | "execution_count": null,
451 | "metadata": {
452 | "slideshow": {
453 | "slide_type": "subslide"
454 | }
455 | },
456 | "outputs": [],
457 | "source": [
458 | "import smtplib\n",
459 | "\n",
460 | "EMAIL_SENDER = os.environ.get('MAIL_USERNAME')\n",
461 | "EMAIL_SENDER_PASSWD = os.environ.get('MAIL_PASSWORD')\n",
462 | "\n",
463 | "@celery.task()\n",
464 | "def send_invitation_email(email):\n",
465 | " server = smtplib.SMTP_SSL('smtp.gmail.com', 465)\n",
466 | " server.login(EMAIL_SENDER, EMAIL_SENDER_PASSWD)\n",
467 | " subject = 'Python Level Up'\n",
468 | " text = 'Welcome to Python Level Up Sample Website!'\n",
469 | " body = '\\r\\n'.join(\n",
470 | " [\n",
471 | " 'To: %s' % email,\n",
472 | " 'From: %s' % EMAIL_SENDER,\n",
473 | " 'Subject: %s' % subject,\n",
474 | " '', text\n",
475 | " ]\n",
476 | " )\n",
477 | " server.sendmail(EMAIL_SENDER, [email], body)"
478 | ]
479 | },
480 | {
481 | "cell_type": "code",
482 | "execution_count": null,
483 | "metadata": {},
484 | "outputs": [],
485 | "source": [
486 | "@app.route('/users', methods=['GET', 'POST'])\n",
487 | "def users():\n",
488 | " if request.method == 'POST':\n",
489 | " email = request.form['email']\n",
490 | " send_invitation_email.delay(email)\n",
491 | " return 'Konto założone'\n",
492 | " else:\n",
493 | " return render_template('add_user.html')"
494 | ]
495 | },
496 | {
497 | "cell_type": "markdown",
498 | "metadata": {
499 | "slideshow": {
500 | "slide_type": "slide"
501 | }
502 | },
503 | "source": [
504 | "# Deploy na heroku\n",
505 | "- https://devcenter.heroku.com/articles/celery-heroku"
506 | ]
507 | },
508 | {
509 | "cell_type": "markdown",
510 | "metadata": {},
511 | "source": [
512 | "### Procfile\n",
513 | "```\n",
514 | "web: gunicorn app:app\n",
515 | "worker: celery worker -A app.celery --loglevel=info\n",
516 | "```"
517 | ]
518 | },
519 | {
520 | "cell_type": "markdown",
521 | "metadata": {},
522 | "source": [
523 | "### Instalacja addona do Redisa\n",
524 | "```\n",
525 | "heroku addons:create heroku-redis -a nazwa_aplikacji\n",
526 | "```"
527 | ]
528 | },
529 | {
530 | "cell_type": "markdown",
531 | "metadata": {},
532 | "source": [
533 | "### requirements.txt\n",
534 | "\n",
535 | "```\n",
536 | "amqp==2.2.2\n",
537 | "billiard==3.5.0.3\n",
538 | "blinker==1.4\n",
539 | "celery==4.1.0\n",
540 | "certifi==2018.4.16\n",
541 | "chardet==3.0.4\n",
542 | "click==6.7\n",
543 | "Flask==0.12.2\n",
544 | "gunicorn==19.7.1\n",
545 | "idna==2.6\n",
546 | "itsdangerous==0.24\n",
547 | "Jinja2==2.10\n",
548 | "kombu==4.1.0\n",
549 | "MarkupSafe==1.0\n",
550 | "pytz==2018.4\n",
551 | "redis==2.10.6\n",
552 | "requests==2.18.4\n",
553 | "urllib3==1.22\n",
554 | "vine==1.1.4\n",
555 | "Werkzeug==0.14.1\n",
556 | "```"
557 | ]
558 | },
559 | {
560 | "cell_type": "markdown",
561 | "metadata": {},
562 | "source": [
563 | "### Nie działa?\n",
564 | "Po deployu może być wymagane ręczne włączenie dyna `worker`"
565 | ]
566 | },
567 | {
568 | "cell_type": "markdown",
569 | "metadata": {
570 | "slideshow": {
571 | "slide_type": "slide"
572 | }
573 | },
574 | "source": [
575 | "# Powtarzanie tasków\n",
576 | "- zdarzają się czasami sytuacje, w których nie udaje się wykonać taska i należy przełożyć go na później\n",
577 | "- służy do tego metoda __retry__ dostępna na obiekcie tasku"
578 | ]
579 | },
580 | {
581 | "cell_type": "markdown",
582 | "metadata": {
583 | "slideshow": {
584 | "slide_type": "subslide"
585 | }
586 | },
587 | "source": [
588 | "## Jak dobrać się do obiektu taska?\n",
589 | "- należy \"zbindować\" obiekt taska (`bind=True`), dzięki czemu jako pierwszy argument do funkcji zostanie przekazany obiekt taska\n",
590 | "```python\n",
591 | "@task(bind=True)\n",
592 | "def add(self, x, y):\n",
593 | " logger.info(self.request.id)\n",
594 | "```"
595 | ]
596 | },
597 | {
598 | "cell_type": "markdown",
599 | "metadata": {
600 | "slideshow": {
601 | "slide_type": "subslide"
602 | }
603 | },
604 | "source": [
605 | "## task.retry()\n",
606 | "\n",
607 | "```python\n",
608 | "import random\n",
609 | "\n",
610 | "@app.task(bind=True)\n",
611 | "def fails_sometimes(self):\n",
612 | " if random.random < 0.5:\n",
613 | " self.retry()\n",
614 | " return 'success'\n",
615 | "```\n",
616 | "\n",
617 | "- `Task.retry` pod spodem rzuca specjalny błąd typu `Retry`\n",
618 | "- możliwe jest nadpisanie domyślnych wartości odpowiedzialnych za powtarzanie tasków:\n",
619 | "```\n",
620 | "@app.task(bind=True, default_retry_delay=30, max_retries=5) \n",
621 | "```"
622 | ]
623 | },
624 | {
625 | "cell_type": "markdown",
626 | "metadata": {
627 | "slideshow": {
628 | "slide_type": "slide"
629 | }
630 | },
631 | "source": [
632 | "# Zapisywanie wyników wywołań poszczególnych tasków\n",
633 | "- należy zdefiniować w instancji __Celery__ `backend`, czyli określić w jakim miejscu chcemy przechowywać rezultaty tasków\n",
634 | "\n",
635 | "```python\n",
636 | "app = Celery('tasks', broker='redis://localhost', backend='redis://localhost')\n",
637 | "```\n",
638 | "- dzięki temu możemy dostawać się do wybiku wywołania taska\n",
639 | "\n",
640 | "```python\n",
641 | "task_result = add.delay(3, 4)\n",
642 | "print(task_result.result) # wyświetla wynik taska\n",
643 | "\n",
644 | "result2 = add.delay(4, 5).get() # czeka na wynik i zwraca go\n",
645 | "```"
646 | ]
647 | },
648 | {
649 | "cell_type": "markdown",
650 | "metadata": {
651 | "slideshow": {
652 | "slide_type": "subslide"
653 | }
654 | },
655 | "source": [
656 | "Nie polecam korzystania z AsyncResult.get() - tracimy w ten sposób korzyść z asynchronicznego wykonywania zadania i wymuszmay wykonanie synchroniczne."
657 | ]
658 | },
659 | {
660 | "cell_type": "markdown",
661 | "metadata": {
662 | "slideshow": {
663 | "slide_type": "slide"
664 | }
665 | },
666 | "source": [
667 | "# Taski cykliczne \n",
668 | "- dzięki schedulerowi __celery beat__ można definiować cykliczne taski, tzn. takie, które mają wykonywać się np. co 30 sekund, w każdy czwartek o 17 itp.\n",
669 | "- ważne jest określenie strefy czasowej z jakiej ma korzystać __Scheduler__ (domyślnie __UTC__)\n",
670 | "\n",
671 | "```python\n",
672 | "timezone = 'Europe/Warsaw'\n",
673 | "```\n",
674 | "\n",
675 | "\n",
676 | "http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html"
677 | ]
678 | },
679 | {
680 | "cell_type": "markdown",
681 | "metadata": {
682 | "slideshow": {
683 | "slide_type": "subslide"
684 | }
685 | },
686 | "source": [
687 | "## Startowanie schedulera\n",
688 | "```bash\n",
689 | "celery -A module_name beat\n",
690 | "```\n",
691 | "\n",
692 | "- można także wystartowac __Schedulera__ razem z __Workerem__\n",
693 | "\n",
694 | "```bash\n",
695 | "celery -A module_name worker -B\n",
696 | "```\n",
697 | "\n",
698 | "- __Celery Beat__ zapisuje czasy ostatnich wywołań __Tasków__ w lokalnym pliku (domyślnie _celerybeat-schedule_), więc musi mieć możliwość zapisu w katalogu, w którym ma zapisywać ten plik\n",
699 | "\n",
700 | "- wybranie innej nazwy/ścieżki do pliku z historią:\n",
701 | "```bash\n",
702 | "celery -A module_name beat -s /different/directory/celerybeat-schedule\n",
703 | "```"
704 | ]
705 | },
706 | {
707 | "cell_type": "code",
708 | "execution_count": null,
709 | "metadata": {
710 | "slideshow": {
711 | "slide_type": "subslide"
712 | }
713 | },
714 | "outputs": [],
715 | "source": [
716 | "# Przyklad z http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#entries\n",
717 | "from celery import Celery\n",
718 | "from celery.schedules import crontab\n",
719 | "\n",
720 | "app = Celery()\n",
721 | "\n",
722 | "@app.on_after_configure.connect\n",
723 | "def setup_periodic_tasks(sender, **kwargs):\n",
724 | " # Calls test('hello') every 10 seconds.\n",
725 | " sender.add_periodic_task(10.0, test.s('hello'), name='add every 10')\n",
726 | "\n",
727 | " # Calls test('world') every 30 seconds\n",
728 | " sender.add_periodic_task(30.0, test.s('world'), expires=10)\n",
729 | "\n",
730 | " # Executes every Monday morning at 7:30 a.m.\n",
731 | " sender.add_periodic_task(\n",
732 | " crontab(hour=7, minute=30, day_of_week=1),\n",
733 | " test.s('Happy Mondays!'),\n",
734 | " )\n",
735 | "\n",
736 | "@app.task\n",
737 | "def test(arg):\n",
738 | " print(arg)"
739 | ]
740 | },
741 | {
742 | "cell_type": "markdown",
743 | "metadata": {
744 | "slideshow": {
745 | "slide_type": "subslide"
746 | }
747 | },
748 | "source": [
749 | "Możliwe jest też definiowanie tasków w konfiguracji, np:\n",
750 | "\n",
751 | "```python\n",
752 | "# Przyklad z http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#entries\n",
753 | "app.conf.beat_schedule = {\n",
754 | " 'add-every-30-seconds': {\n",
755 | " 'task': 'tasks.add',\n",
756 | " 'schedule': 30.0,\n",
757 | " 'args': (16, 16)\n",
758 | " },\n",
759 | "}\n",
760 | "```"
761 | ]
762 | },
763 | {
764 | "cell_type": "markdown",
765 | "metadata": {
766 | "slideshow": {
767 | "slide_type": "slide"
768 | }
769 | },
770 | "source": [
771 | "# DZIĘKUJEMY!!!!\n",
772 | "\n",
773 | "https://medium.freecodecamp.org/python-collection-of-my-favorite-articles-8469b8455939"
774 | ]
775 | }
776 | ],
777 | "metadata": {
778 | "celltoolbar": "Slideshow",
779 | "kernelspec": {
780 | "display_name": "Python 3",
781 | "language": "python",
782 | "name": "python3"
783 | },
784 | "language_info": {
785 | "codemirror_mode": {
786 | "name": "ipython",
787 | "version": 3
788 | },
789 | "file_extension": ".py",
790 | "mimetype": "text/x-python",
791 | "name": "python",
792 | "nbconvert_exporter": "python",
793 | "pygments_lexer": "ipython3",
794 | "version": "3.6.1"
795 | }
796 | },
797 | "nbformat": 4,
798 | "nbformat_minor": 2
799 | }
800 |
--------------------------------------------------------------------------------
/07_celery/przyklady/tasks.py:
--------------------------------------------------------------------------------
1 | from celery import Celery
2 |
3 | app = Celery('tasks', broker='redis://localhost', backend='redis://localhost')
4 |
5 | @app.task
6 | def add(x, y):
7 | return x + y
8 |
--------------------------------------------------------------------------------
/07_celery/przyklady/web_app/Procfile:
--------------------------------------------------------------------------------
1 | web: gunicorn app:app
2 | worker: celery worker -A app.celery --loglevel=INFO
3 |
--------------------------------------------------------------------------------
/07_celery/przyklady/web_app/app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import smtplib
3 |
4 | from celery import Celery
5 | from flask import (
6 | Flask,
7 | request,
8 | render_template,
9 | )
10 | import requests
11 | import time
12 |
13 | app = Flask(__name__)
14 | app.config['CELERY_BROKER_URL'] = os.environ['REDIS_URL']
15 |
16 | celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'],
17 | backend=app.config['CELERY_BROKER_URL'])
18 | celery.conf.update(app.config)
19 |
20 | EMAIL_SENDER = os.environ.get('MAIL_USERNAME')
21 | EMAIL_SENDER_PASSWD = os.environ.get('MAIL_PASSWORD')
22 |
23 |
24 | @celery.task()
25 | def add(x, y):
26 | return x + y
27 |
28 |
29 | @celery.task()
30 | def make_request():
31 | requests.get('http://httpbin.org/delay/5')
32 |
33 |
34 | @celery.task()
35 | def send_invitation_email(email):
36 | server = smtplib.SMTP_SSL('smtp.gmail.com', 465)
37 | server.login(EMAIL_SENDER, EMAIL_SENDER_PASSWD)
38 | subject = 'Python Level Up'
39 | text = 'Welcome to Python Level Up Sample Website!'
40 | body = '\r\n'.join(
41 | [
42 | 'To: %s' % email,
43 | 'From: %s' % EMAIL_SENDER,
44 | 'Subject: %s' % subject,
45 | '', text
46 | ]
47 | )
48 | server.sendmail(EMAIL_SENDER, [email], body)
49 |
50 |
51 | @app.route('/sync_request')
52 | def make_sync_request():
53 | start = time.time()
54 | make_request()
55 | end = time.time()
56 | return str(end - start)
57 |
58 |
59 | @app.route('/async_request')
60 | def make_async_request():
61 | start = time.time()
62 | make_request.delay()
63 | end = time.time()
64 | return str(end - start)
65 |
66 |
67 | @app.route('/wait')
68 | def wait_for_result():
69 | result = add.delay(3, 5).get()
70 | return str(result)
71 |
72 |
73 | @app.route('/users', methods=['GET', 'POST'])
74 | def users():
75 | if request.method == 'POST':
76 | email = request.form['email']
77 | send_invitation_email.delay(email)
78 | return 'Konto założone'
79 | else:
80 | return render_template('add_user.html')
81 |
82 |
83 | if __name__ == '__main__':
84 | app.run(debug=True)
85 |
--------------------------------------------------------------------------------
/07_celery/przyklady/web_app/requirements.txt:
--------------------------------------------------------------------------------
1 | amqp==2.2.2
2 | billiard==3.5.0.3
3 | blinker==1.4
4 | celery==4.1.0
5 | certifi==2018.4.16
6 | chardet==3.0.4
7 | click==6.7
8 | Flask==0.12.2
9 | gunicorn==19.7.1
10 | idna==2.6
11 | itsdangerous==0.24
12 | Jinja2==2.10
13 | kombu==4.1.0
14 | MarkupSafe==1.0
15 | pytz==2018.4
16 | redis==2.10.6
17 | requests==2.18.4
18 | urllib3==1.22
19 | vine==1.1.4
20 | Werkzeug==0.14.1
--------------------------------------------------------------------------------
/07_celery/przyklady/web_app/templates/add_user.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Dodawanie użytkownika
6 |
7 |
8 |
13 |
14 |
--------------------------------------------------------------------------------
/07_celery/twitter_use_case.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DaftAcademy/python_levelup_2018/d05320ee90af11fff1ae5e2c24cead6fca9e1abf/07_celery/twitter_use_case.png
--------------------------------------------------------------------------------
/07_celery/web_application_flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DaftAcademy/python_levelup_2018/d05320ee90af11fff1ae5e2c24cead6fca9e1abf/07_celery/web_application_flow.png
--------------------------------------------------------------------------------
/07_celery/web_application_flow_celery.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DaftAcademy/python_levelup_2018/d05320ee90af11fff1ae5e2c24cead6fca9e1abf/07_celery/web_application_flow_celery.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Python Level UP
2 | 
3 |
4 | ## Plan zajęć
5 | 
6 |
7 | ## Ważne linki
8 | ### Informacje organizacyjne
9 | [https://www.facebook.com/events/1552802084832333/](https://www.facebook.com/events/1552802084832333/)
10 |
11 | ### Materiały z zajęć
12 | [https://github.com/daftcode/python_levelup_2018](https://github.com/daftcode/python_levelup_2018)
13 |
14 | ## Kontakt
15 | [python.levelup@daftcode.pl](python.levelup@daftcode.pl)
16 | ## Przygotowanie środowiska pracy przed zajęciami
17 | ### Instalacja Python 3.6
18 | Żeby nie tracić czasu w trakcie warsztatu, zależałoby nam żebyście przyszli na zajęcia z zainstalowaną odpowiednią wersją Pythona. Poniżej opisana jest krótka instrukcja instalacji dla najpopularniejszych systemów operacyjnych.
19 |
20 | W przypadku problemów z instalacją możesz się z nami kontaktować mailowo ;)
21 | #### Windows
22 | Wejdź na stronę [https://www.python.org/downloads/release/python-363/](https://www.python.org/downloads/release/python-363/) i pobierz odpowiedni instalator z sekcji `Files` - `Windows x86-64 executable installer` dla systemu 64-bitowego lub `Windows x86 executable installer` dla systemy 32-bitowego.
23 |
24 | Uruchom pobrany instalator. Zaznacz opcję `Add Python 3.6 to PATH`, a następnie kliknij `Install Now`.
25 | #### macOS
26 | Wejdź na stronę [https://www.python.org/downloads/release/python-363/](https://www.python.org/downloads/release/python-363/) i pobierz odpowiedni instalator z sekcji `Files` - `Mac OS X 64-bit/32-bit installer`. Uruchom pobrany plik i dokończ instalację.
27 | #### Linux
28 | Istnieje duża szansa, że masz zainstalowanego odpowiedniego pythona na swoim komputerze. W celu sprawdzenia jaka wersja jest zainstalowana, wpisz w terminalu:
29 | ```bazaar
30 | python3 --version
31 | ```
32 | Jeżelu uzyskasz wynik `Python 3.6.x` - jesteś gotowy na zajęcia. W przypadku, gdy nie zostanie odnaleziona komenda `python3` lub zainstalowana będzie niższa wersja niż `Python 3.6`, należy podążać za kolejnymi krokami, zależnymi od systemu, który posiadasz.
33 | ##### Debian lub Ubuntu
34 | Użyj w terminalu następującej komendy:
35 | ```bazaar
36 | sudo apt-get install python3.6
37 | ```
38 | Dla wersji Ubuntu starszych niż 16.10 powyższa komenda może nie zadziałać. W takiej sytuacji należy skorzystać z deadsnakes PPA:
39 | ```bazaar
40 | sudo add-apt-repository ppa:deadsnakes/ppa
41 | sudo apt-get update
42 | sudo apt-get install python3.6
43 | ```
44 | ##### Fedora (22+)
45 | Użyj w terminalu następującej komendy:
46 | ```bazaar
47 | sudo dnf install python3
48 | ```
49 | Dla starszych wersji Fedory możesz dostać błąd mówiący o tym, że komenda `dnf` nie została znaleziona. W takiej sytuacji należy skorzystać z komendy `yum`.
50 | ##### openSUSE
51 | Użyj w terminalu następującej komendy:
52 | ```bazaar
53 | sudo zypper install python3
54 | ```
55 | ### Sprawdzenie, czy Python 3.6 jest zainstalowany
56 | Wpisz w terminalu następującą komendę:
57 | ```bazaar
58 | python3.6 --version
59 | ```
60 | Jeżeli powyższa komenda zwróci wynik `Python 3.6.x` oznacza to, że masz zainstalowaną odpowiednią wersję Pythona.
61 |
62 | Na Windowsie powyższa komenda może nie zadziałać. Wtedy należy użyć w `Wierszu polecenia`:
63 | ```bazaar
64 | python --version
65 | ```
66 | Powinno ono zwrócić wynik `Python 3.6.x`.
67 | ### Wybór edytora tekstu
68 | Programowanie w Pythonie nie wymaga żadnych specjalistycznych narzędzi - wystarczy korzystać z edytora tekstu. Na zajęciach możesz korzystać z dowolnego edytora. Jeżeli nie wiesz co wybrać, polecamy Sublime Text [https://www.sublimetext.com/](https://www.sublimetext.com/).
69 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DaftAcademy/python_levelup_2018/d05320ee90af11fff1ae5e2c24cead6fca9e1abf/logo.png
--------------------------------------------------------------------------------
/plan_zajec.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DaftAcademy/python_levelup_2018/d05320ee90af11fff1ae5e2c24cead6fca9e1abf/plan_zajec.png
--------------------------------------------------------------------------------
/zadania_rekrutacyjne/Zadanie_1/Zadanie_1_polecenie.md:
--------------------------------------------------------------------------------
1 | # Zadanie 1
2 | W plikach word_0.txt, word_1.txt, ..., word_29.txt znajdują się
3 | pojedyncze słowa składające się z małych i wielkich liter ascii.
4 |
5 | Pliki word_0.txt, word_1.txt, ..., word_29.txt są spakowane w
6 | zadanie_1_words.zip.
7 |
8 | Twoim zadaniem jest policzenie, ile razy każda z liter występuje we
9 | wszystkich plikach. Małe i wielkie litery liczymy razem.
10 |
11 | Wynik należy przedstawić w postaci napisu
12 | ....
13 | Litery powinny być uszeregowane w kolejności alfabetycznej.
14 |
15 | ## Przykład:
16 | Dla uproszczenia załóżmy, że mamy tylko dwa pliki: word_0.txt i word_1.txt.
17 |
18 | word_0.txt:
19 | cCbAba
20 |
21 | word_1.txt:
22 | BBbCc
23 |
24 | Oczekiwany wynik:
25 | a2b5c4
--------------------------------------------------------------------------------
/zadania_rekrutacyjne/Zadanie_1/zadanie_1_words.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DaftAcademy/python_levelup_2018/d05320ee90af11fff1ae5e2c24cead6fca9e1abf/zadania_rekrutacyjne/Zadanie_1/zadanie_1_words.zip
--------------------------------------------------------------------------------
/zadania_rekrutacyjne/Zadanie_2/Zadanie_2_polecenie.md:
--------------------------------------------------------------------------------
1 | # Zadanie 2
2 | Podaj sumę wszystkich początkowych liczb pierwszych, aż do liczby numer 1234567 (włącznie).
3 | W zadaniu chodzi o sumę pierwszych 1234567 liczb pierwszych, a nie o sumę liczb pierwszych mniejszych od 1234567.
4 |
5 | info: https://pl.wikipedia.org/wiki/Liczba_pierwsza
6 |
7 | ## Przykłady:
8 | Suma liczb pierwszych aż do liczby nr 5 (włacznie) = `sum([2, 3, 5, 7, 11])` = 28
9 | Suma liczb pierwszych aż do liczby nr 11 (włacznie) = `sum([2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31])` = 160
10 |
--------------------------------------------------------------------------------
/zadania_rekrutacyjne/Zadanie_3/Zadanie_3_polecenie.md:
--------------------------------------------------------------------------------
1 | # Zadanie 3
2 | Proszę znaleźć ścieżkę z największą sumą elementów w "trójkącie".
3 | Przechodzić można tylko z góry na dół w lewo lub na dół w prawo.
4 | Każda liczba w trójkącie jest z zakresu od 10 do 99.
5 | Trójkąt jest w pliku zadanie_3_triangle_small.txt
6 |
7 | ## Przykład:
8 |
9 | Trójkąt:
10 | ```
11 | 1
12 | 2 3
13 | 4 5 6
14 | ```
15 |
16 | Ma następujące ścieżki (wszystkie są podane):
17 |
18 | 1, 2, 4 = 7
19 | 1, 2, 5 = 8
20 | 1, 3, 5 = 9
21 | 1, 3, 6 = 10
22 |
23 | Ścieżka z największą sumą to 1, 3, 6 więc odpowiedzią jest 10.
--------------------------------------------------------------------------------
/zadania_rekrutacyjne/Zadanie_3/zadanie_3_triangle_small.txt:
--------------------------------------------------------------------------------
1 | 29
2 | 80 82
3 | 53 60 27
4 | 35 21 81 90
5 | 10 33 19 40 81
6 | 67 41 58 49 43 42
7 | 89 65 25 60 39 80 65
8 | 61 86 14 75 59 79 17 59
9 | 82 41 20 48 43 76 16 35 84
10 | 23 80 58 73 98 19 85 62 73 32
11 | 28 86 67 61 32 48 81 31 40 55 90
12 | 70 19 30 22 78 32 91 17 65 32 68 89
13 | 21 88 41 68 84 77 20 14 10 43 55 48 99
14 | 12 58 74 77 45 88 38 50 94 40 34 30 51 15
15 |
--------------------------------------------------------------------------------
/zadania_rekrutacyjne/Zadanie_4/Zadanie_4_polecenie.md:
--------------------------------------------------------------------------------
1 | # Zadanie 4
2 | Proszę znaleźć ścieżkę z największą sumą elementów w "trójkącie".
3 | Przechodzić można tylko z góry na dół w lewo lub na dół w prawo.
4 | Każda liczba w trójkącie jest z zakresu od 10 do 99.
5 | Trójkąt jest w pliku zadanie_4_triangle_big.txt
6 |
7 |
8 | ## Przykład:
9 |
10 | Trójkąt:
11 | ```
12 | 1
13 | 2 3
14 | 4 5 6
15 | ```
16 |
17 | Ma następujące ścieżki (wszystkie są podane):
18 |
19 | 1, 2, 4 = 7
20 | 1, 2, 5 = 8
21 | 1, 3, 5 = 9
22 | 1, 3, 6 = 10
23 |
24 | Ścieżka z największą sumą to 1, 3, 6 więc odpowiedzią jest 10.
--------------------------------------------------------------------------------
/zajecia_1/README.md:
--------------------------------------------------------------------------------
1 | # Przygotowanie do pierwszych zajęć
2 | W trakcie pierwszych zajęć będziemy tworzyć aplikację webową przy użyciu frameworku `Flask`, a następnie będziemy ją wrzucać na serwer `Heroku` przy użyciu systemu kontroli wersji `Git`.
3 | Zachęcamy do przygotowania środowiska pracy jeszcze przed pierwszymi zajęciami zgodnie z poniższą instrukcją:
4 |
5 | ## 1. Stworzenie środowiska wirtualnego
6 | Środowisko wirtuale tworzymy za pomocą komendy `python3 -m venv`, a następnie podajemy ścieżkę pod którą ma on zostać utworzony.
7 | ```
8 | python3 -m venv /path/to/new/virtual/environment
9 | ```
10 |
11 | Więcej o środowiskach wirtualnych: https://docs.python.org/3.6/library/venv.html
12 |
13 | ## 2. Instalacja Flaska w stworzonym środowisku wirtualnym
14 | Przed instalacją zewnętrznych zależności należy aktywować virtualenv:
15 |
16 | Windows:
17 | ```
18 | \path\to\new\virtual\environment\Scripts\activate.bat
19 | ```
20 |
21 | Linux:
22 | ```
23 | source /path/to/new/virtual/environment/bin/activate
24 | ```
25 |
26 | Następnie instalujemy Flaska za pomocą komendy
27 | ```
28 | pip install Flask
29 | ```
30 | Więcej o instalowaniu dodatkowych bibliotek w Pythonie: https://docs.python.org/3.6/installing/index.html
31 |
32 | ## 3. Instalacja gita
33 | Przy instalacji Gita można skorzystać z bardzo dobrej instrukcji znajdującej się w tutorialu Django Girls: https://tutorial.djangogirls.org/en/deploy/#installing-git .
34 | Więcej o gicie: https://git-scm.com/
35 |
36 | ## 4. Założenie konta na heroku
37 | Załóż konto na https://signup.heroku.com/ . Jest to serwis ułatwiający wrzucanie aplikacji webowych na serwer.
38 |
39 | ## 5. Instalacja Heroku CLI
40 | Zainstaluj narzędzie Heroku CLI zgodnie z instrukcją pod następującym linkiem: https://devcenter.heroku.com/articles/heroku-cli .
41 |
--------------------------------------------------------------------------------
/zajecia_1/praca_domowa/README.md:
--------------------------------------------------------------------------------
1 | # Praca domowa 1
2 |
3 | Głównym celem pierwszej pracy domowej jest opublikowanie w Internecie naszej aplikacji napisanej w Pythonie. Pozwoli to nam w dalszych etapach kursu na skupienie się na innych zagadnieniach związanych z web developmentem w Pythonie.
4 |
5 | Podczas wykładu przekazaliśmy wskazówki jak zrobić deploy aplikacji na **Heroku.com**. Ten sposób uznamy więc za preferowany. Nic jednak nie stoi na przeszkodzie abyście wrzucili swoją aplikację gdzie indziej, np.: AWS, Google Cloud, własny hosting etc. Ocenimy tylko te prace które będą dostępne publicznie.
6 |
7 | Poniższe wymagania opisują jakie ścieżki (ang. _paths_) powinna obsługiwać Twoja aplikacja.
8 |
9 | Przykładowa aplikacja dostępna jest tutaj: https://homework-01.herokuapp.com
10 |
11 | Odpowiedź podaj **wyłącznie** przez: https://goo.gl/forms/bmidcmQNUEIzrujA3
12 |
13 |
14 | ## Wymagania:
15 |
16 | 1. Stwórz ścieżkę `/` która zwracać będzie "Hello, World!". Tak po prostu :)
17 |
18 | 2. Stwórz ścieżkę `/now` która będzie zwracać dzisiejszą datę i godzinę:
19 | * Ważne jest prawidłowe sformatowanie wartości zwracanej przez funkcję endpointu.
20 | * Użyj formatu identycznego do tego: `2018-03-07 15:37:58.610113`. Godzinę podaj w UTC.
21 |
22 | 3. Stwórz ścieżkę `/user-agent` która zwróci informacje o user agencie użytkownika.
23 | * Wykorzystaj informacje o requeście podawane przez `flask`.
24 | * Format wymagany: `{device-type} / {os} / {browser}`,
25 | np: `PC / Linux / Chrome 64.0.3282`
26 | * _Nie wymyślaj koła od nowa_ ;-)
27 |
28 | 4. Stwórz ścieżkę `/counter` która zliczać będzię odwiedziny tej ścieżki.
29 | * W tym zadaniu ważne jest aby znaleźć miejsce w którym będzie można zapisać ilość odwiedzin od ostatniego uruchomienia aplikacji na serwerze
30 | * na tym etapie kursu nie bawimy się w bazy danych
31 | * być może spostrzeżesz bardzo ciekawe zachowanie ;-)
32 |
--------------------------------------------------------------------------------
/zajecia_1/praca_domowa/rozwiazanie/hello-world.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from flask import Flask, request
4 | from user_agents import parse
5 | app = Flask(__name__)
6 | app.counter = 0
7 |
8 |
9 | @app.route('/')
10 | def hello():
11 | return "Hello, World!"
12 |
13 |
14 | @app.route('/now')
15 | def now():
16 | # Tutaj może się pojawić problem, że zwracany element nie jest "callable"
17 | # Można go rozwiązać w prosty sposób formatując wartość wyjściową.
18 | return str(datetime.utcnow())
19 |
20 |
21 | @app.route('/counter')
22 | def counter():
23 | app.counter += 1
24 | return str(app.counter)
25 |
26 |
27 | @app.route('/user-agent')
28 | def ua():
29 | ua = parse(request.headers.get('User-Agent'))
30 | return str(ua)
31 |
--------------------------------------------------------------------------------
/zajecia_1/warszat_01.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {
6 | "slideshow": {
7 | "slide_type": "slide"
8 | }
9 | },
10 | "source": [
11 | ""
12 | ]
13 | },
14 | {
15 | "cell_type": "markdown",
16 | "metadata": {
17 | "slideshow": {
18 | "slide_type": "subslide"
19 | }
20 | },
21 | "source": [
22 | ""
23 | ]
24 | },
25 | {
26 | "cell_type": "markdown",
27 | "metadata": {
28 | "slideshow": {
29 | "slide_type": "subslide"
30 | }
31 | },
32 | "source": [
33 | "# Ważne linki\n",
34 | "\n",
35 | "- Informacje organizacyjne: https://www.facebook.com/events/1552802084832333/\n",
36 | "- Materiały do kursu: https://github.com/daftcode/python_levelup_2018"
37 | ]
38 | },
39 | {
40 | "cell_type": "markdown",
41 | "metadata": {
42 | "slideshow": {
43 | "slide_type": "slide"
44 | }
45 | },
46 | "source": [
47 | "# Flask(a) dla każdego\n",
48 | "## Pierwsza aplikacja webowa\n",
49 | "### Jakub Szponder\n",
50 | "#### Python Level Up, 08.03.2018"
51 | ]
52 | },
53 | {
54 | "cell_type": "markdown",
55 | "metadata": {
56 | "slideshow": {
57 | "slide_type": "slide"
58 | }
59 | },
60 | "source": [
61 | "# Aplikacja webowa\n",
62 | "- aplikacja działająca w modelu klient-serwer\n",
63 | "- klient: najczęściej przeglądarka (ale może to być też np. inna aplikacja)\n",
64 | "- serwer: serwer aplikacji webowej, który nasłuchuje na połączenia na wybranych portach (zazwyczaj 80 lub 443)\n",
65 | "- klient z serwerem komunikują się za pomocą protokołu HTTP"
66 | ]
67 | },
68 | {
69 | "cell_type": "markdown",
70 | "metadata": {
71 | "slideshow": {
72 | "slide_type": "slide"
73 | }
74 | },
75 | "source": [
76 | "# Protokół HTTP\n",
77 | "- połączenie inicjowane przez klienta\n",
78 | "- bezstanowe\n",
79 | "- korzysta z protokołu TCP"
80 | ]
81 | },
82 | {
83 | "cell_type": "markdown",
84 | "metadata": {
85 | "slideshow": {
86 | "slide_type": "subslide"
87 | }
88 | },
89 | "source": [
90 | "## Request HTTP\n",
91 | "```\n",
92 | "METODA URL WERSJA_HTTP\n",
93 | "<0 lub więcej> NAGŁÓWKI \n",
94 | "\n",
95 | " TREŚĆ_ZAPYTANIA\n",
96 | "```"
97 | ]
98 | },
99 | {
100 | "cell_type": "markdown",
101 | "metadata": {
102 | "slideshow": {
103 | "slide_type": "subslide"
104 | }
105 | },
106 | "source": [
107 | "### (Najważniejsze) Metody HTTP\n",
108 | "- **GET** - pobranie zasobu\n",
109 | "- **POST** - przesyłanie danych od klienta do serwera, najczęściej służy do stworzenia zasobu\n",
110 | "- DELETE - żądanie usunięcia zasobu\n",
111 | "- PUT - przesyłanie danych od klienta do serwera, najczęściej służy do zastąpienia istniejącego zasobu\n",
112 | "- PATCH - przesyłanie danych od klienta do serwera, najczęściej służy do częściowej aktualizacji istniejącego zasobu"
113 | ]
114 | },
115 | {
116 | "cell_type": "markdown",
117 | "metadata": {
118 | "slideshow": {
119 | "slide_type": "subslide"
120 | }
121 | },
122 | "source": [
123 | "### Pozostałe metody HTTP\n",
124 | "- HEAD - pobranie nagłówków dla danego zasobu\n",
125 | "- OPTIONS - żądanie informacji o metodach dostępnych dla danego zasobu\n",
126 | "- CONNECT - wykorzystane w trakcie tunelowania z użyciem serwerów pośredniczących\n",
127 | "- TRACE - służy do diagnostyki"
128 | ]
129 | },
130 | {
131 | "cell_type": "markdown",
132 | "metadata": {
133 | "slideshow": {
134 | "slide_type": "subslide"
135 | }
136 | },
137 | "source": [
138 | "### URL\n",
139 | "ścieżka do zasobu, w formacie:\n",
140 | "```\n",
141 | "scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]\n",
142 | "```\n",
143 | "\n",
144 | "Przykład\n",
145 | "```\n",
146 | "pełen adres: https://pl.wikipedia.org/wiki/Hypertext_Transfer_Protocol\n",
147 | "adres serwera: https://pl.wikipedia.org\n",
148 | "ścieżka do zasobu: /wiki/Hypertext_Transfer_Protocol\n",
149 | "```\n",
150 | "\n",
151 | "https://en.wikipedia.org/wiki/URL"
152 | ]
153 | },
154 | {
155 | "cell_type": "markdown",
156 | "metadata": {
157 | "slideshow": {
158 | "slide_type": "subslide"
159 | }
160 | },
161 | "source": [
162 | "### Wersja HTTP\n",
163 | "- HTTP/0.9 (rok 1991)\n",
164 | "- HTTP/1.0 (rok 1996)\n",
165 | "- HTTP/1.1 (rok 1997)\n",
166 | "- HTTP/2.0 (rok 2015)"
167 | ]
168 | },
169 | {
170 | "cell_type": "markdown",
171 | "metadata": {
172 | "slideshow": {
173 | "slide_type": "subslide"
174 | }
175 | },
176 | "source": [
177 | "### Nagłówki HTTP\n",
178 | "- Host\n",
179 | "- User-Agent\n",
180 | "- Content-Type\n",
181 | "- Cookie\n",
182 | "\n",
183 | "więcej: https://pl.wikipedia.org/wiki/Lista_nag%C5%82%C3%B3wk%C3%B3w_HTTP"
184 | ]
185 | },
186 | {
187 | "cell_type": "markdown",
188 | "metadata": {
189 | "slideshow": {
190 | "slide_type": "subslide"
191 | }
192 | },
193 | "source": [
194 | "### Przykładowe requesty HTTP\n",
195 | "\n",
196 | "GET:\n",
197 | "```\n",
198 | "GET /hello.htm HTTP/1.1\n",
199 | "User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)\n",
200 | "Host: www.tutorialspoint.com\n",
201 | "Accept-Language: en-us\n",
202 | "Accept-Encoding: gzip, deflate\n",
203 | "Connection: Keep-Alive\n",
204 | "```\n",
205 | "\n",
206 | "POST:\n",
207 | "```\n",
208 | "POST /cgi-bin/process.cgi HTTP/1.1\n",
209 | "User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)\n",
210 | "Host: www.tutorialspoint.com\n",
211 | "Content-Type: application/x-www-form-urlencoded\n",
212 | "Content-Length: 49\n",
213 | "Accept-Language: en-us\n",
214 | "Accept-Encoding: gzip, deflate\n",
215 | "Connection: Keep-Alive\n",
216 | "\n",
217 | "licenseID=string&content=string&/paramsXML=string\n",
218 | "```\n"
219 | ]
220 | },
221 | {
222 | "cell_type": "markdown",
223 | "metadata": {
224 | "slideshow": {
225 | "slide_type": "subslide"
226 | }
227 | },
228 | "source": [
229 | "## Response HTTP\n",
230 | "```\n",
231 | "WERSJA_HTTP STATUS_HTTP OPIS_STATUSU\n",
232 | "<0 lub więcej> NAGŁÓWKI \n",
233 | "\n",
234 | " TREŚĆ_ODPOWIEDZI\n",
235 | "```"
236 | ]
237 | },
238 | {
239 | "cell_type": "markdown",
240 | "metadata": {
241 | "slideshow": {
242 | "slide_type": "subslide"
243 | }
244 | },
245 | "source": [
246 | "### Grupy statusów HTTP (i przykładowe statusy)\n",
247 | "#### 1XX - informacyjne\n",
248 | "#### 2XX - sukces\n",
249 | "- 200 OK\n",
250 | "\n",
251 | "#### 3XX - przekierowanie\n",
252 | "- 301 Moved Permanently\n",
253 | "- 302 Found\n",
254 | "\n",
255 | "#### 4XX - błąd klienta\n",
256 | "- 400 Bad Request\n",
257 | "- 401 Unauthorized\n",
258 | "- 403 Forbidden\n",
259 | "- 404 Not Found\n",
260 | "\n",
261 | "#### 5XX - błąd serwera\n",
262 | "- 500 Internal Server Error\n",
263 | "- 503 Service Unavailable"
264 | ]
265 | },
266 | {
267 | "cell_type": "markdown",
268 | "metadata": {
269 | "slideshow": {
270 | "slide_type": "subslide"
271 | }
272 | },
273 | "source": [
274 | "### Przykładowe odpowiedzi HTTP\n",
275 | "Sukces:\n",
276 | "```\n",
277 | "HTTP/1.1 200 OK\n",
278 | "Date: Mon, 27 Jul 2009 12:28:53 GMT\n",
279 | "Server: Apache/2.2.14 (Win32)\n",
280 | "Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT\n",
281 | "Content-Length: 88\n",
282 | "Content-Type: text/html\n",
283 | "Connection: Closed\n",
284 | "\n",
285 | "\n",
286 | "\n",
287 | "Hello, World!
\n",
288 | "\n",
289 | "\n",
290 | "```\n",
291 | "\n",
292 | "Błąd:\n",
293 | "```\n",
294 | "HTTP/1.1 404 Not Found\n",
295 | "Date: Sun, 18 Oct 2012 10:36:20 GMT\n",
296 | "Server: Apache/2.2.14 (Win32)\n",
297 | "Content-Length: 230\n",
298 | "Connection: Closed\n",
299 | "Content-Type: text/html; charset=iso-8859-1\n",
300 | "```"
301 | ]
302 | },
303 | {
304 | "cell_type": "markdown",
305 | "metadata": {
306 | "slideshow": {
307 | "slide_type": "subslide"
308 | }
309 | },
310 | "source": [
311 | "# HTTPS\n",
312 | "- szyfrowana wersja HTTP\n",
313 | "- szyfruje dane przy pomocy protokołu TLS\n",
314 | "- zapobiega przechwytywaniu i zmienianiu danych\n",
315 | "- wywołanie protokołu zaczyna się od `https://`\n",
316 | "- przeglądarki pokazują nam kłódki"
317 | ]
318 | },
319 | {
320 | "cell_type": "markdown",
321 | "metadata": {
322 | "slideshow": {
323 | "slide_type": "slide"
324 | }
325 | },
326 | "source": [
327 | "# Framework do aplikacji webowych\n",
328 | "- framework ułatwiający tworzenie aplikacji internetowych\n",
329 | "- daje narzędzia do obsługi **routingu** i **templatingu**\n",
330 | "- dzięki użyciu frameworku NIE MUSIMY za każdym razem wymyślać koła na nowo"
331 | ]
332 | },
333 | {
334 | "cell_type": "markdown",
335 | "metadata": {
336 | "slideshow": {
337 | "slide_type": "subslide"
338 | }
339 | },
340 | "source": [
341 | "## Routing\n",
342 | "- mapowanie odpowiedniego URLa zasobu na funkcję, która powinna obsłużyć żądanie\n",
343 | "- łatwy sposób na określenie, że za obsłużenie żądania dla URL `/hello` odpowiedzialna jest funkcja `hello()`"
344 | ]
345 | },
346 | {
347 | "cell_type": "markdown",
348 | "metadata": {
349 | "slideshow": {
350 | "slide_type": "subslide"
351 | }
352 | },
353 | "source": [
354 | "## Templating\n",
355 | "- ułatwienie generowania dynamicznych szablonów HTML"
356 | ]
357 | },
358 | {
359 | "cell_type": "markdown",
360 | "metadata": {
361 | "slideshow": {
362 | "slide_type": "subslide"
363 | }
364 | },
365 | "source": [
366 | "## Pythonowe frameworki\n",
367 | "- Django\n",
368 | "- Pyramid\n",
369 | "- Flask\n",
370 | "- i wiele innych\n",
371 | "\n",
372 | "https://www.airpair.com/python/posts/django-flask-pyramid"
373 | ]
374 | },
375 | {
376 | "cell_type": "markdown",
377 | "metadata": {
378 | "slideshow": {
379 | "slide_type": "subslide"
380 | }
381 | },
382 | "source": [
383 | "## Flask\n",
384 | "- ułatwia **routing** i **templating**\n",
385 | "- minimalna aplikacja to kilka linijek\n",
386 | "- prawie nic nie narzuca (daje możliwość wyboru odpowiednich narzędzi przez programistę)\n",
387 | "- w przeciwieństwie do np. Django, które daje bardzo dużo, ale też podejmuje wiele decyzji za programistę (np. wybór ORM)"
388 | ]
389 | },
390 | {
391 | "cell_type": "markdown",
392 | "metadata": {
393 | "slideshow": {
394 | "slide_type": "slide"
395 | }
396 | },
397 | "source": [
398 | "# Instalacja Flaska\n",
399 | "Stworzenie virtualenva\n",
400 | "```\n",
401 | "python3.6 -m venv level_up_env\n",
402 | "```\n",
403 | "Aktywowanie virtualenva Linux/Mac\n",
404 | "```\n",
405 | "source level_up_env/bin/activate\n",
406 | "```\n",
407 | "Aktywowanie virtualenva Windows\n",
408 | "```\n",
409 | "level_up_env\\Scripts\\activate.bat\n",
410 | "```\n",
411 | "Instalacja Flaska\n",
412 | "\n",
413 | "```\n",
414 | "pip install Flask\n",
415 | "```"
416 | ]
417 | },
418 | {
419 | "cell_type": "markdown",
420 | "metadata": {
421 | "slideshow": {
422 | "slide_type": "slide"
423 | }
424 | },
425 | "source": [
426 | "# Pierwsza aplikacja we Flasku"
427 | ]
428 | },
429 | {
430 | "cell_type": "code",
431 | "execution_count": null,
432 | "metadata": {
433 | "slideshow": {
434 | "slide_type": "subslide"
435 | }
436 | },
437 | "outputs": [],
438 | "source": [
439 | "# app.py\n",
440 | "from flask import Flask\n",
441 | "app = Flask(__name__)\n",
442 | "\n",
443 | "\n",
444 | "@app.route('/')\n",
445 | "def hello():\n",
446 | " return 'Hello!'\n",
447 | "\n",
448 | "\n",
449 | "if __name__ == '__main__':\n",
450 | " app.run(debug=True)"
451 | ]
452 | },
453 | {
454 | "cell_type": "markdown",
455 | "metadata": {
456 | "slideshow": {
457 | "slide_type": "fragment"
458 | }
459 | },
460 | "source": [
461 | "- Flask(`NAME`) tworzy obiekt aplikacji o nazwie `NAME`\n",
462 | "- dekorator @app.route(`ROUTE_NAME`) nad funkcją określa, że funkcja ta jest odpowiedzialna za obsługę requestów na URL `ROUTE_NAME`\n",
463 | "- funkcja widoku zwraca body responsu\n",
464 | "- `app.run()` startuje aplikację - nasłuchuje ona domyślnie na porcie 5000\n",
465 | "- uruchomienie aplikacji w trybie debug (`app.run(debug=True)`) powoduje, że aplikacja przeładowuje się po wykryciu zmian w plikach"
466 | ]
467 | },
468 | {
469 | "cell_type": "markdown",
470 | "metadata": {
471 | "slideshow": {
472 | "slide_type": "subslide"
473 | }
474 | },
475 | "source": [
476 | "## Startowanie aplikacji\n",
477 | "```\n",
478 | "python app.py\n",
479 | "```"
480 | ]
481 | },
482 | {
483 | "cell_type": "markdown",
484 | "metadata": {
485 | "slideshow": {
486 | "slide_type": "fragment"
487 | }
488 | },
489 | "source": [
490 | "```\n",
491 | "* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)\n",
492 | "```"
493 | ]
494 | },
495 | {
496 | "cell_type": "markdown",
497 | "metadata": {
498 | "slideshow": {
499 | "slide_type": "subslide"
500 | }
501 | },
502 | "source": [
503 | "## Obiekt request\n",
504 | "- Funkcje widoków uruchamiane są w kontekście requestu\n",
505 | "- by uzyskać dostęp do obiektu requestu, należy odwołać się do `flask.request`\n",
506 | "- obiekt **request** zawiera wszystkie podstawowe informacje o requeście - metodę, url, body, headery itd.\n",
507 | "- Więcej: http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseRequest"
508 | ]
509 | },
510 | {
511 | "cell_type": "code",
512 | "execution_count": null,
513 | "metadata": {
514 | "slideshow": {
515 | "slide_type": "subslide"
516 | }
517 | },
518 | "outputs": [],
519 | "source": [
520 | "# app.py\n",
521 | "from flask import request\n",
522 | "\n",
523 | "\n",
524 | "@app.route('/request')\n",
525 | "def request_info():\n",
526 | " return f'request method: {request.method} url: {request.url} headers: {request.headers}'"
527 | ]
528 | },
529 | {
530 | "cell_type": "markdown",
531 | "metadata": {
532 | "slideshow": {
533 | "slide_type": "slide"
534 | }
535 | },
536 | "source": [
537 | "# Deploy!\n",
538 | "- aplikację wrzucamy na serwer **Heroku**\n",
539 | "- do wrzucenia kodu korzystamy z systemu kontroli wersji **Git**\n",
540 | "- żeby aplikacja pythonowa działała na serwerze, potrzebny jest serwer **WSGI**\n",
541 | "- serwerem WSGI z którego skorzystamy jest **Gunicorn**\n",
542 | "- żeby zainstalować odpowiednie zależności na serwerze potrzebujemy pliku **requirements.txt**"
543 | ]
544 | },
545 | {
546 | "cell_type": "markdown",
547 | "metadata": {
548 | "slideshow": {
549 | "slide_type": "slide"
550 | }
551 | },
552 | "source": [
553 | "## Serwer wbudowany Flaska\n",
554 | "*While lightweight and easy to use, Flask’s built-in server is not suitable for production as it doesn’t scale well and by default serves only one request at a time*\n",
555 | "- bardzo wolny\n",
556 | "- potrafi obsługiwać jednocześnie tylko jeden request\n",
557 | "\n",
558 | "Więcej: http://flask.pocoo.org/docs/0.12/deploying/"
559 | ]
560 | },
561 | {
562 | "cell_type": "markdown",
563 | "metadata": {
564 | "slideshow": {
565 | "slide_type": "slide"
566 | }
567 | },
568 | "source": [
569 | "## Heroku\n",
570 | "- *container-based cloud Platform as a Service (PaaS)*\n",
571 | "- usługa umożliwiająca udostępnienie aplikacji webowej bez martwienia się infrastrukturą\n",
572 | "- udostępnia bardzo wygodny sposób deployowania\n",
573 | "- umożliwia darmowe deployowanie aplikacji\n",
574 | "\n",
575 | "Więcej: https://www.heroku.com/"
576 | ]
577 | },
578 | {
579 | "cell_type": "markdown",
580 | "metadata": {
581 | "slideshow": {
582 | "slide_type": "slide"
583 | }
584 | },
585 | "source": [
586 | "## Git\n",
587 | "- **Git** to **rozproszony system kontroli wersji**\n",
588 | "- stworzony w 2005 roku przez Linusa Torvaldsa"
589 | ]
590 | },
591 | {
592 | "cell_type": "markdown",
593 | "metadata": {
594 | "slideshow": {
595 | "slide_type": "subslide"
596 | }
597 | },
598 | "source": [
599 | "### System kontroli wersji\n",
600 | "- system, który śledzi zmiany dokonywane na plikach i umożliwia powracanie do dowolnej wcześniejszej wersji\n",
601 | "- zapisuje kto, kiedy i jakie wprowadził zmiany\n",
602 | "- pozwala odwrócić zmiany, które coś popsuły\n",
603 | "- ułatwia pracę w wiele osób nad jednym projektem"
604 | ]
605 | },
606 | {
607 | "cell_type": "markdown",
608 | "metadata": {
609 | "slideshow": {
610 | "slide_type": "subslide"
611 | }
612 | },
613 | "source": [
614 | "### Rozproszony system kontroli wersji\n",
615 | "- każdy klient trzyma całą historię\n",
616 | "- w przypadku awarii jednego z serwerów, repozytorium dowolnego klienta może je zastąpić\n",
617 | "- możliwa jest praca z kilkoma serwerami zdalnymi"
618 | ]
619 | },
620 | {
621 | "cell_type": "markdown",
622 | "metadata": {
623 | "slideshow": {
624 | "slide_type": "subslide"
625 | }
626 | },
627 | "source": [
628 | "### Zalety gita\n",
629 | "- szybkość - większość operacji wykonywana lokalnie\n",
630 | "- wydajność w trakcie pracy z dużymi projektami\n",
631 | "- wspiera rozgałęzienia w historii rozwoju projektu"
632 | ]
633 | },
634 | {
635 | "cell_type": "markdown",
636 | "metadata": {
637 | "slideshow": {
638 | "slide_type": "subslide"
639 | }
640 | },
641 | "source": [
642 | "### Instalacja gita\n",
643 | "- dobrze opisane w tutorialu Django Girls: https://tutorial.djangogirls.org/en/deploy/#installing-git"
644 | ]
645 | },
646 | {
647 | "cell_type": "markdown",
648 | "metadata": {
649 | "slideshow": {
650 | "slide_type": "subslide"
651 | }
652 | },
653 | "source": [
654 | "### Podstawowe komendy\n",
655 | "\n",
656 | "#### tworzenie nowego repozytorium\n",
657 | "- `git init` - tworzy nowe repozytorium\n",
658 | "\n",
659 | "#### tworzenie kopii istniejącego repozytorium\n",
660 | "- `git clone /path/to/repository` - kopiowanie lokalnego repozytorium\n",
661 | "- `git clone username@host:/path/to/repository` - kopiowanie zdalnego repozytorium\n",
662 | "\n",
663 | "#### dodawanie zmian\n",
664 | "- `git add path/to/file` - dodanie pliku do zbioru zmian, które mają zostać zatwierdzone (**staged changes**)\n",
665 | "- `git commit -m \"Commit message\"` - zatwierdzenie **staged changes** i dodanie ich do repozytorium\n",
666 | "\n",
667 | "#### sprawdzanie statusu plików w repozytorium\n",
668 | "- `git status` - wyświetla status poszczególnych plików w repozytorium\n",
669 | "- `git diff` - wyświetlenie jakie zmiany zostały dokonane i nie zostały jeszcze dodane do **staged changes**\n",
670 | "\n",
671 | "#### wysyłanie zmian na zdalny serwer\n",
672 | "- `git push remote_server_name master` - wypchnięcie zmian z brancha **master** na serwer **remote_server_name**\n",
673 | "\n",
674 | "#### pobieranie zmian ze zdalnego repozytorium\n",
675 | "- `git pull` - ściągnięcie zmian ze zdalnego repozytorium i połączenie ich z lokalnymi zmianami"
676 | ]
677 | },
678 | {
679 | "cell_type": "markdown",
680 | "metadata": {
681 | "slideshow": {
682 | "slide_type": "subslide"
683 | }
684 | },
685 | "source": [
686 | "### Więcej o Gicie\n",
687 | "- http://rogerdudler.github.io/git-guide/ - krótko o podstawowych komendach (trochę więcej niż tutaj)\n",
688 | "- https://git-scm.com/book/pl/v1/Pierwsze-kroki lub https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control - dokładniejszy opis działania\n",
689 | "- https://try.github.io - interaktywny tutorial"
690 | ]
691 | },
692 | {
693 | "cell_type": "markdown",
694 | "metadata": {
695 | "slideshow": {
696 | "slide_type": "slide"
697 | }
698 | },
699 | "source": [
700 | "## WSGI\n",
701 | "- *Web Server Gateway Interface*\n",
702 | "- specyfikacja, która określa, w jaki sposób aplikacje web serwery mają się komunikować z aplikacjami\n",
703 | "- standard w Pythonie, opisany w PEP 3333: https://www.python.org/dev/peps/pep-3333/\n",
704 | "- frameworki webowe w Pythonie są zgodne z tą specyfikacją\n",
705 | "\n",
706 | "Więcej: http://wsgi.readthedocs.io/en/latest/"
707 | ]
708 | },
709 | {
710 | "cell_type": "markdown",
711 | "metadata": {
712 | "slideshow": {
713 | "slide_type": "slide"
714 | }
715 | },
716 | "source": [
717 | "## Gunicorn\n",
718 | "- jedna z wielu implementacji serwera **WSGI**\n",
719 | "- **NIE TRZEBA** instalować tego lokalnie. Ważne jest żeby było to zainstalowane na serwerze - tym bardziej, że na Windowsie **nie działa** ;)\n",
720 | "- instalacja:\n",
721 | "```\n",
722 | "pip install gunicorn\n",
723 | "```\n",
724 | "- uruchamianie aplikacji z użyciem gunicorna za pomocą jednej komendy\n",
725 | "```\n",
726 | "gunicorn app:app\n",
727 | "```\n",
728 | "- możliwe jest też uruchamianie aplikacji z użyciem gunicorna za pomocą pliku **Procfile**\n",
729 | "- **Gunicorn** ma wiele konfigurowalnych opcji. M.in. można zdefiniować liczbę workerów, która ma zostać uruchomiona (domyślnie na **Heroku** to dwa)\n",
730 | "\n",
731 | "Więcej: http://gunicorn.org/#docs"
732 | ]
733 | },
734 | {
735 | "cell_type": "markdown",
736 | "metadata": {
737 | "slideshow": {
738 | "slide_type": "slide"
739 | }
740 | },
741 | "source": [
742 | "### Procfile\n",
743 | "- Plik określający w jaki sposób ma być uruchomiona nasza aplikacja\n",
744 | "- **Heroku** korzysta z niego do serwowania aplikacji\n",
745 | "- **Procfile** dla naszej aplikacji:\n",
746 | "```\n",
747 | "web: gunicorn app:app\n",
748 | "```\n",
749 | "- Plik ten definiuje jeden proces, o nazwie *web* i komendę, która ma być dla niego wykonana\n",
750 | "- Na podstawie nazwy *web* **Heroku** wie, że jest to proces, który będzie nasłuchiwał na requesty **HTTP**"
751 | ]
752 | },
753 | {
754 | "cell_type": "markdown",
755 | "metadata": {
756 | "slideshow": {
757 | "slide_type": "slide"
758 | }
759 | },
760 | "source": [
761 | "### requirements.txt\n",
762 | "- plik zawierający listę zależności, które należy zainstalować przy użyciu komendy **pip install**\n",
763 | "- instalowanie zależności na podstawie pliku **requirements.txt**:\n",
764 | "```\n",
765 | "pip install -r requirements.txt\n",
766 | "```\n",
767 | "- tworzenie pliku **requirements.txt** na podstawie aktualnego środowiska:\n",
768 | "```\n",
769 | "pip freeze > requirements.txt\n",
770 | "```\n",
771 | "- `pip freeze` to komenda, która zwraca listę zależności zainstalowanych w aktualnym virtualenvie\n",
772 | "- **requirements.txt** powinien zawierać dokładnie określone wersje wszystkich zależności\n",
773 | "- dzięki temu \"na produkcji\" będą te same wersje zależności, na których testowaliśmy aplikację lokalnie\n",
774 | "- przydaje się też, gdy wiele osób pracuje nad jednym projektem"
775 | ]
776 | },
777 | {
778 | "cell_type": "markdown",
779 | "metadata": {
780 | "slideshow": {
781 | "slide_type": "slide"
782 | }
783 | },
784 | "source": [
785 | "### requirements.txt dla naszej aplikacji\n",
786 | "```\n",
787 | "click==6.7\n",
788 | "Flask==0.12.2\n",
789 | "gunicorn==19.7.1\n",
790 | "itsdangerous==0.24\n",
791 | "Jinja2==2.10\n",
792 | "MarkupSafe==1.0\n",
793 | "Werkzeug==0.14.1\n",
794 | "```"
795 | ]
796 | },
797 | {
798 | "cell_type": "markdown",
799 | "metadata": {
800 | "slideshow": {
801 | "slide_type": "slide"
802 | }
803 | },
804 | "source": [
805 | "## Instrukcja deployu na Heroku\n",
806 | "1. Załóż konto na **Heroku**: https://signup.heroku.com\n",
807 | "2. Stwórz nową aplikację. Nadaj jej nazwę, wybierz region Europa\n",
808 | "3. Wybierz metodę deployu: **Heroku Git**\n",
809 | "4. Pobierz **Heroku CLI**\n",
810 | "5. Stwórz plik **requirements.txt**: `pip freeze > requirements.txt`\n",
811 | "6. Dodaj do pliku **requirements.txt** linię `gunicorn==19.7.1`\n",
812 | "7. Dodaj plik **Procfile** o treści `web: gunicorn app:app` <- Pierwsze `app` to nazwa modułu (pliku) z naszą aplikacją (jeżeli inaczej nazwałaś/nazwałeś ten plik, to trzeba użyć Twojej nazwy), drugie to nazwa zmiennej na którą przypisaliśmy obiekt Flask (w linijce `app = Flask(__name__)`)\n",
813 | "8. Zaloguj się do **Heroku CLI** `heroku login`\n",
814 | "9. Stwórz repozytorium git `git init`\n",
815 | "10. Dodaj zdalny serwer `heroku git:remote -a NAZWA-APLIKACJI-NA-HEROKU` (nazwa podana w punkcie 2. instrukcji)\n",
816 | "11. Dodaj pliki do git `git add Procfile app.py requirements.txt`\n",
817 | "12. Zatwierdź dodane zmiany `git commit -m 'My first deploy'`\n",
818 | "13. Wypchnij zmiay na serwer heroku `git push heroku master`\n",
819 | "14. Wejdż na https://NAZWA-APLIKACJI.herokuapp.com/"
820 | ]
821 | },
822 | {
823 | "cell_type": "markdown",
824 | "metadata": {
825 | "slideshow": {
826 | "slide_type": "slide"
827 | }
828 | },
829 | "source": [
830 | "# Jest zadanie domowe ;)"
831 | ]
832 | }
833 | ],
834 | "metadata": {
835 | "celltoolbar": "Slideshow",
836 | "kernelspec": {
837 | "display_name": "Python 3",
838 | "language": "python",
839 | "name": "python3"
840 | },
841 | "language_info": {
842 | "codemirror_mode": {
843 | "name": "ipython",
844 | "version": 3
845 | },
846 | "file_extension": ".py",
847 | "mimetype": "text/x-python",
848 | "name": "python",
849 | "nbconvert_exporter": "python",
850 | "pygments_lexer": "ipython3",
851 | "version": "3.6.1"
852 | }
853 | },
854 | "nbformat": 4,
855 | "nbformat_minor": 2
856 | }
857 |
--------------------------------------------------------------------------------
/zajecia_1/web_app/Procfile:
--------------------------------------------------------------------------------
1 | web: gunicorn app:app
--------------------------------------------------------------------------------
/zajecia_1/web_app/app.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 | from flask import request
3 |
4 | app = Flask(__name__)
5 |
6 |
7 | @app.route('/')
8 | def hello():
9 | return 'Hello!'
10 |
11 |
12 | @app.route('/request')
13 | def request_info():
14 | return f'request method: {request.method} url: {request.url} headers: {request.headers}'
15 |
16 |
17 | if __name__ == '__main__':
18 | app.run(debug=True)
19 |
--------------------------------------------------------------------------------
/zajecia_1/web_app/requirements.txt:
--------------------------------------------------------------------------------
1 | click==6.7
2 | Flask==0.12.2
3 | gunicorn==19.7.1
4 | itsdangerous==0.24
5 | Jinja2==2.10
6 | MarkupSafe==1.0
7 | Werkzeug==0.14.1
8 |
--------------------------------------------------------------------------------
/zajecia_2/Jeszcze_jedna.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {
6 | "slideshow": {
7 | "slide_type": "slide"
8 | }
9 | },
10 | "source": [
11 | "# Jeszcze jedna\n",
12 | "## Flask ciąg dalszy\n",
13 | "\n",
14 | "### Marcin Jaroszewski\n",
15 | "### 15.III.2018, Python Level UP"
16 | ]
17 | },
18 | {
19 | "cell_type": "markdown",
20 | "metadata": {
21 | "slideshow": {
22 | "slide_type": "subslide"
23 | }
24 | },
25 | "source": [
26 | ""
27 | ]
28 | },
29 | {
30 | "cell_type": "markdown",
31 | "metadata": {
32 | "slideshow": {
33 | "slide_type": "subslide"
34 | }
35 | },
36 | "source": [
37 | ""
38 | ]
39 | },
40 | {
41 | "cell_type": "markdown",
42 | "metadata": {
43 | "slideshow": {
44 | "slide_type": "subslide"
45 | }
46 | },
47 | "source": [
48 | "## Tutoriale flask\n",
49 | "- https://www.tutorialspoint.com/flask/index.htm\n",
50 | "- http://flask.pocoo.org/docs/0.12/tutorial/"
51 | ]
52 | },
53 | {
54 | "cell_type": "markdown",
55 | "metadata": {
56 | "slideshow": {
57 | "slide_type": "slide"
58 | }
59 | },
60 | "source": [
61 | "# Po co w ogóle są webaplikacje?"
62 | ]
63 | },
64 | {
65 | "cell_type": "markdown",
66 | "metadata": {
67 | "slideshow": {
68 | "slide_type": "subslide"
69 | }
70 | },
71 | "source": [
72 | "https://pages.apigee.com/rs/apigee/images/api-design-ebook-2012-03.pdf"
73 | ]
74 | },
75 | {
76 | "cell_type": "markdown",
77 | "metadata": {
78 | "slideshow": {
79 | "slide_type": "slide"
80 | }
81 | },
82 | "source": [
83 | "# HTML \"dynamiczny\""
84 | ]
85 | },
86 | {
87 | "cell_type": "markdown",
88 | "metadata": {
89 | "slideshow": {
90 | "slide_type": "subslide"
91 | }
92 | },
93 | "source": [
94 | "## Podejście prymitywne/chałupnicze"
95 | ]
96 | },
97 | {
98 | "cell_type": "markdown",
99 | "metadata": {
100 | "slideshow": {
101 | "slide_type": "subslide"
102 | }
103 | },
104 | "source": [
105 | "Podstawowa aplikacja flask:\n",
106 | "```python\n",
107 | "from flask import Flask, request\n",
108 | "app = Flask(__name__)\n",
109 | "\n",
110 | "@app.route(\"/\")\n",
111 | "def hello():\n",
112 | "\treturn 'hello from flask'\n",
113 | "\n",
114 | "if __name__ == '__main__':\n",
115 | " app.run(debug=True)\n",
116 | "```"
117 | ]
118 | },
119 | {
120 | "cell_type": "markdown",
121 | "metadata": {
122 | "slideshow": {
123 | "slide_type": "subslide"
124 | }
125 | },
126 | "source": [
127 | "### Pobieranie danych z querystring\n",
128 | "Standardowe zapytanie z przeglądarki to GET.\n",
129 | "\n",
130 | "GET z założenia nie przesyła danych do serwera na \"trwałe\" (do tego służy POST).\n",
131 | "\n",
132 | "Ale GET ma możliwość przesłania pewnej ilości danych do serwera za pomocą **querystring**.\n",
133 | "\n",
134 | "Te dane nie powinny zostać zapisane w serwerze na stałe, ale mogą posłużyć do wygenerowania odpowiedzi."
135 | ]
136 | },
137 | {
138 | "cell_type": "markdown",
139 | "metadata": {
140 | "slideshow": {
141 | "slide_type": "subslide"
142 | }
143 | },
144 | "source": [
145 | "```python\n",
146 | "@app.route(\"/request_query_string_discovery\")\n",
147 | "def print_request():\n",
148 | " print(dir(request))\n",
149 | " print('request.args:', request.args)\n",
150 | " print('type(request.args):', type(request.args))\n",
151 | " print('request.query_string:', request.query_string)\n",
152 | " return ''\n",
153 | "```"
154 | ]
155 | },
156 | {
157 | "cell_type": "markdown",
158 | "metadata": {
159 | "slideshow": {
160 | "slide_type": "subslide"
161 | }
162 | },
163 | "source": [
164 | "To co nas interesuje to: `request.args`\n",
165 | "```python\n",
166 | "type(request.args): \n",
167 | "```\n",
168 | "dokumentacja:\n",
169 | "1. http://werkzeug.pocoo.org/docs/0.14/datastructures/#werkzeug.datastructures.ImmutableMultiDict\n",
170 | "2. http://werkzeug.pocoo.org/docs/0.14/datastructures/#werkzeug.datastructures.MultiDict"
171 | ]
172 | },
173 | {
174 | "cell_type": "markdown",
175 | "metadata": {
176 | "slideshow": {
177 | "slide_type": "subslide"
178 | }
179 | },
180 | "source": [
181 | "### Proste wstawianie danych od użytkownika w nasz HTML"
182 | ]
183 | },
184 | {
185 | "cell_type": "markdown",
186 | "metadata": {
187 | "slideshow": {
188 | "slide_type": "subslide"
189 | }
190 | },
191 | "source": [
192 | "http://127.0.0.1:5000/request_query_string_based_response?ala=ma_kota"
193 | ]
194 | },
195 | {
196 | "cell_type": "markdown",
197 | "metadata": {
198 | "slideshow": {
199 | "slide_type": "subslide"
200 | }
201 | },
202 | "source": [
203 | "### Złośliwy użytkownik i znaczniki HTML w inpucie"
204 | ]
205 | },
206 | {
207 | "cell_type": "markdown",
208 | "metadata": {
209 | "slideshow": {
210 | "slide_type": "subslide"
211 | }
212 | },
213 | "source": [
214 | "Pierwszy złośliwy url ze wstrzyknięciem html: http://127.0.0.1:5000/request_query_string_based_response?+=%3C%21DOCTYPE+html%3E%3Chtml%3E%3Chead%3E%3Ctitle%3EPage+Title%3C%2Ftitle%3E%3C%2Fhead%3E%3Cbody%3E%3Ch1%3EHACKED%21%21%21%3C%2Fh1%3E%3C%2Fbody%3E%3C%2Fhtml%3E"
215 | ]
216 | },
217 | {
218 | "cell_type": "markdown",
219 | "metadata": {
220 | "slideshow": {
221 | "slide_type": "subslide"
222 | }
223 | },
224 | "source": [
225 | "```html\n",
226 | "Page TitleHACKED!!!
\n",
227 | "```"
228 | ]
229 | },
230 | {
231 | "cell_type": "code",
232 | "execution_count": 1,
233 | "metadata": {
234 | "scrolled": true,
235 | "slideshow": {
236 | "slide_type": "subslide"
237 | }
238 | },
239 | "outputs": [
240 | {
241 | "name": "stdout",
242 | "output_type": "stream",
243 | "text": [
244 | "+=%3C%21DOCTYPE+html%3E%3Chtml%3E%3Chead%3E%3Ctitle%3EPage+Title%3C%2Ftitle%3E%3C%2Fhead%3E%3Cbody%3E%3Ch1%3EHACKED%21%21%21%3C%2Fh1%3E%3C%2Fbody%3E%3C%2Fhtml%3E\n"
245 | ]
246 | }
247 | ],
248 | "source": [
249 | "evil_input = 'Page TitleHACKED!!!
'\n",
250 | "import urllib.parse\n",
251 | "query_string = urllib.parse.urlencode({' ': evil_input})\n",
252 | "print(query_string)"
253 | ]
254 | },
255 | {
256 | "cell_type": "markdown",
257 | "metadata": {
258 | "slideshow": {
259 | "slide_type": "subslide"
260 | }
261 | },
262 | "source": [
263 | "Drugi złośliwy url ze wstrzyknięciem html i javascript: http://127.0.0.1:5000/request_query_string_based_response?+=%3C%21DOCTYPE+html%3E%3Chtml%3E%3Chead%3E%3Ctitle%3EPage+Title%3C%2Ftitle%3E%3C%2Fhead%3E%3Cscript%3Ewindow.location.replace%28%22https%3A%2F%2Fhaveibeenpwned.com%2F%22%29%3B%3C%2Fscript%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E"
264 | ]
265 | },
266 | {
267 | "cell_type": "markdown",
268 | "metadata": {
269 | "slideshow": {
270 | "slide_type": "subslide"
271 | }
272 | },
273 | "source": [
274 | "```html\n",
275 | "Page Title\n",
276 | "```"
277 | ]
278 | },
279 | {
280 | "cell_type": "code",
281 | "execution_count": 2,
282 | "metadata": {
283 | "slideshow": {
284 | "slide_type": "subslide"
285 | }
286 | },
287 | "outputs": [
288 | {
289 | "name": "stdout",
290 | "output_type": "stream",
291 | "text": [
292 | "+=%3C%21DOCTYPE+html%3E%3Chtml%3E%3Chead%3E%3Ctitle%3EPage+Title%3C%2Ftitle%3E%3C%2Fhead%3E%3Cscript%3Ewindow.location.replace%28%22https%3A%2F%2Fhaveibeenpwned.com%2F%22%29%3B%3C%2Fscript%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E\n"
293 | ]
294 | }
295 | ],
296 | "source": [
297 | "more_evil_input = 'Page Title'\n",
298 | "query_string = urllib.parse.urlencode({' ': more_evil_input})\n",
299 | "print(query_string)"
300 | ]
301 | },
302 | {
303 | "cell_type": "markdown",
304 | "metadata": {
305 | "slideshow": {
306 | "slide_type": "subslide"
307 | }
308 | },
309 | "source": [
310 | "Ataków zbliżonych do przedstawionych jest bardzo wiele. Ciężko się zamodzielnie przed nimi bronić. W dodadtku samodzielne składanie html też nie jest takie łatwe. Na szczęście Flask i wiele innych frameworków webowych oferuje nam system templatek."
311 | ]
312 | },
313 | {
314 | "cell_type": "markdown",
315 | "metadata": {
316 | "slideshow": {
317 | "slide_type": "slide"
318 | }
319 | },
320 | "source": [
321 | "# Templatki"
322 | ]
323 | },
324 | {
325 | "cell_type": "markdown",
326 | "metadata": {
327 | "slideshow": {
328 | "slide_type": "subslide"
329 | }
330 | },
331 | "source": [
332 | "## Jinja2"
333 | ]
334 | },
335 | {
336 | "cell_type": "markdown",
337 | "metadata": {
338 | "slideshow": {
339 | "slide_type": "subslide"
340 | }
341 | },
342 | "source": [
343 | "Dokumentacja: http://jinja.pocoo.org/docs/2.10/"
344 | ]
345 | },
346 | {
347 | "cell_type": "markdown",
348 | "metadata": {
349 | "slideshow": {
350 | "slide_type": "subslide"
351 | }
352 | },
353 | "source": [
354 | "```html\n",
355 | "\n",
356 | "\n",
357 | "\n",
358 | " Wyrenderowane z templatki\n",
359 | "\n",
360 | "\n",
361 | " {% for key, value in query_string_data|dictsort %}\n",
362 | " {{ key }}: {{ value }}
\n",
363 | " {% endfor %}\n",
364 | "\n",
365 | "\n",
366 | "```"
367 | ]
368 | },
369 | {
370 | "cell_type": "markdown",
371 | "metadata": {
372 | "slideshow": {
373 | "slide_type": "subslide"
374 | }
375 | },
376 | "source": [
377 | "Templatki we Flask same robią autoescape danych, które mogą popsuć stronę."
378 | ]
379 | },
380 | {
381 | "cell_type": "markdown",
382 | "metadata": {
383 | "slideshow": {
384 | "slide_type": "slide"
385 | }
386 | },
387 | "source": [
388 | "# Routing"
389 | ]
390 | },
391 | {
392 | "cell_type": "markdown",
393 | "metadata": {
394 | "slideshow": {
395 | "slide_type": "subslide"
396 | }
397 | },
398 | "source": [
399 | "### Pobieranie danych z url\n",
400 | "\n",
401 | "```python\n",
402 | "@app.route(\"/simple_path_tmpl/\")\n",
403 | "def simple_path_tmpl(sample_variable):\n",
404 | " print(sample_variable)\n",
405 | " print(type(sample_variable))\n",
406 | " print(app.url_map)\n",
407 | " return render_template(\n",
408 | " 'route_description_tmpl.html',\n",
409 | " value=sample_variable,\n",
410 | " my_type=type(sample_variable),\n",
411 | " my_id=id(sample_variable),\n",
412 | " )\n",
413 | "```"
414 | ]
415 | },
416 | {
417 | "cell_type": "markdown",
418 | "metadata": {
419 | "slideshow": {
420 | "slide_type": "subslide"
421 | }
422 | },
423 | "source": [
424 | "### Pobieranie danych z url z automatyczną konwersją\n",
425 | "```python\n",
426 | "@app.route(\"/simple_path_int/\")\n",
427 | "def simple_path_int(sample_variable):\n",
428 | " print(sample_variable)\n",
429 | " print(type(sample_variable))\n",
430 | " print(app.url_map)\n",
431 | " return render_template(\n",
432 | " 'route_description_tmpl.html',\n",
433 | " value=sample_variable,\n",
434 | " my_type=type(sample_variable),\n",
435 | " my_id=id(sample_variable),\n",
436 | " )\n",
437 | "```"
438 | ]
439 | },
440 | {
441 | "cell_type": "markdown",
442 | "metadata": {
443 | "slideshow": {
444 | "slide_type": "subslide"
445 | }
446 | },
447 | "source": [
448 | "### Obsługa dowolnej ścieżki\n",
449 | "```python\n",
450 | "@app.route(\"/path/\")\n",
451 | "def path_all(my_path):\n",
452 | " print(my_path)\n",
453 | " print(type(my_path))\n",
454 | " print(app.url_map)\n",
455 | " return render_template(\n",
456 | " 'route_description_tmpl.html',\n",
457 | " value=my_path,\n",
458 | " my_type=type(my_path),\n",
459 | " my_id=id(my_path),\n",
460 | " )\n",
461 | "```"
462 | ]
463 | },
464 | {
465 | "cell_type": "markdown",
466 | "metadata": {
467 | "slideshow": {
468 | "slide_type": "slide"
469 | }
470 | },
471 | "source": [
472 | "# Obsługa różnych metod"
473 | ]
474 | },
475 | {
476 | "cell_type": "markdown",
477 | "metadata": {
478 | "slideshow": {
479 | "slide_type": "subslide"
480 | }
481 | },
482 | "source": [
483 | "Metody HTTP służą do tego, żeby dla tego samego url zachowanie/odpowiedź serwera były inne. W miarę możliwości zgodne ze znaczeniem użytej metody."
484 | ]
485 | },
486 | {
487 | "cell_type": "markdown",
488 | "metadata": {
489 | "slideshow": {
490 | "slide_type": "subslide"
491 | }
492 | },
493 | "source": [
494 | "## GET - pobieranie zasobu\n",
495 | "```python\n",
496 | "@app.route(\"/person/\")\n",
497 | "def person_info(person_name):\n",
498 | " global persons\n",
499 | " # do samodzielnego zastanowienia się, czy to bezpieczne\n",
500 | " person = persons.get(person_name)\n",
501 | " return render_template(\n",
502 | " 'person_tmpl.html',\n",
503 | " name=person.get('name'),\n",
504 | " surname=person.get('surname'),\n",
505 | " occupation=person.get('occupation'),\n",
506 | " )\n",
507 | "```"
508 | ]
509 | },
510 | {
511 | "cell_type": "markdown",
512 | "metadata": {
513 | "slideshow": {
514 | "slide_type": "subslide"
515 | }
516 | },
517 | "source": [
518 | "## Postman i inne podobne\n",
519 | "Do wysyłania requestów innych niż GET i lepszej kontroli na requestami.\n",
520 | "Można też napisać sobie skrypty w python (np z użyciem biblioteki requests).\n",
521 | "- https://www.getpostman.com/"
522 | ]
523 | },
524 | {
525 | "cell_type": "markdown",
526 | "metadata": {
527 | "slideshow": {
528 | "slide_type": "subslide"
529 | }
530 | },
531 | "source": [
532 | "## POST - dodawanie zasobu\n",
533 | "```python\n",
534 | "@app.route(\"/person/\", methods=['GET', 'POST'])\n",
535 | "def person_info(person_name):\n",
536 | "\tif request.method == 'GET':\n",
537 | "\t\treturn get_person_info(person_name)\n",
538 | "\telif request.method == 'POST':\n",
539 | "\t\treturn post_person_info(person_name)\n",
540 | "```"
541 | ]
542 | },
543 | {
544 | "cell_type": "markdown",
545 | "metadata": {
546 | "slideshow": {
547 | "slide_type": "subslide"
548 | }
549 | },
550 | "source": [
551 | "```python\n",
552 | "def post_person_info(person_name):\n",
553 | "\tdata = request.get_json()\n",
554 | "\tnew_person = {\n",
555 | "\t\t'name': data.get('name'),\n",
556 | "\t\t'surname': data.get('surname'),\n",
557 | "\t\t'occupation': data.get('occupation')\n",
558 | "\t}\n",
559 | "\tglobal persons\n",
560 | "\tpersons[data.get('name')] = new_person\n",
561 | "\treturn 'OK'\n",
562 | "```"
563 | ]
564 | },
565 | {
566 | "cell_type": "markdown",
567 | "metadata": {
568 | "slideshow": {
569 | "slide_type": "slide"
570 | }
571 | },
572 | "source": [
573 | "# Cookies"
574 | ]
575 | },
576 | {
577 | "cell_type": "markdown",
578 | "metadata": {
579 | "slideshow": {
580 | "slide_type": "slide"
581 | }
582 | },
583 | "source": [
584 | "## Co to są ciasteczka"
585 | ]
586 | },
587 | {
588 | "cell_type": "markdown",
589 | "metadata": {
590 | "slideshow": {
591 | "slide_type": "slide"
592 | }
593 | },
594 | "source": [
595 | "## Ustawianie i odczyt ciasteczka"
596 | ]
597 | },
598 | {
599 | "cell_type": "markdown",
600 | "metadata": {
601 | "slideshow": {
602 | "slide_type": "subslide"
603 | }
604 | },
605 | "source": [
606 | "```python\n",
607 | "@app.route(\"/my_cookies\")\n",
608 | "def cookies():\n",
609 | "\tcookie_secret = request.cookies.get('cookie_secret')\n",
610 | "\tresp = make_response(\n",
611 | "\t\trender_template(\n",
612 | "\t\t\t'cookies_tmpl.html', cookie_secret=cookie_secret\n",
613 | "\t\t)\n",
614 | "\t)\n",
615 | "\tresp.set_cookie('cookie_secret', 'I am cookie')\n",
616 | "\treturn resp\n",
617 | "```"
618 | ]
619 | },
620 | {
621 | "cell_type": "markdown",
622 | "metadata": {
623 | "slideshow": {
624 | "slide_type": "slide"
625 | }
626 | },
627 | "source": [
628 | "# Bezpieczeństwo"
629 | ]
630 | },
631 | {
632 | "cell_type": "markdown",
633 | "metadata": {
634 | "slideshow": {
635 | "slide_type": "subslide"
636 | }
637 | },
638 | "source": [
639 | "Każdy może nam w naszej aplikacji czytać, dodawać, zmieniać, kasować dowolne zasoby. A to raczej niedobrze."
640 | ]
641 | },
642 | {
643 | "cell_type": "markdown",
644 | "metadata": {
645 | "slideshow": {
646 | "slide_type": "subslide"
647 | }
648 | },
649 | "source": [
650 | "# BasicAuth"
651 | ]
652 | },
653 | {
654 | "cell_type": "markdown",
655 | "metadata": {
656 | "slideshow": {
657 | "slide_type": "subslide"
658 | }
659 | },
660 | "source": [
661 | "## Logowanie przez ciasteczko"
662 | ]
663 | }
664 | ],
665 | "metadata": {
666 | "celltoolbar": "Slideshow",
667 | "kernelspec": {
668 | "display_name": "Python 3",
669 | "language": "python",
670 | "name": "python3"
671 | },
672 | "language_info": {
673 | "codemirror_mode": {
674 | "name": "ipython",
675 | "version": 3
676 | },
677 | "file_extension": ".py",
678 | "mimetype": "text/x-python",
679 | "name": "python",
680 | "nbconvert_exporter": "python",
681 | "pygments_lexer": "ipython3",
682 | "version": "3.6.4"
683 | }
684 | },
685 | "nbformat": 4,
686 | "nbformat_minor": 2
687 | }
688 |
--------------------------------------------------------------------------------
/zajecia_2/hello_flask.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, request, render_template, make_response
2 |
3 | persons = {
4 | 'Jan': {
5 | 'name': 'Jan',
6 | 'surname': 'Kowalski',
7 | 'occupation': 'Warszawa',
8 | },
9 | }
10 |
11 | app = Flask(__name__)
12 |
13 |
14 | @app.route("/")
15 | def hello():
16 | return 'hello from flask'
17 |
18 |
19 | @app.route("/request_query_string_discovery")
20 | def print_request():
21 | print(dir(request))
22 | print('request.args:', request.args)
23 | print('type(request.args):', type(request.args))
24 | print('request.query_string:', request.query_string)
25 | return ''
26 |
27 |
28 | @app.route('/request_query_string_based_response')
29 | def print_request_2():
30 | line_tmpl = '{}: {}'
31 | lines = []
32 | for key in request.args.keys():
33 | lines.append(line_tmpl.format(key, request.args[key]))
34 | return '\n'.join(lines)
35 |
36 |
37 | @app.route('/request_query_string_based_response_with_template')
38 | def print_request_3():
39 | query_string_data = dict(request.args)
40 | return render_template(
41 | 'querystring_render_tmpl.html',
42 | query_string_data=query_string_data
43 | )
44 |
45 |
46 | @app.route("/simple_path_tmpl/")
47 | def simple_path_tmpl(sample_variable):
48 | print(sample_variable)
49 | print(type(sample_variable))
50 | print(app.url_map)
51 | return render_template(
52 | 'route_description_tmpl.html',
53 | value=sample_variable,
54 | my_type=type(sample_variable),
55 | my_id=id(sample_variable),
56 | )
57 |
58 |
59 | @app.route("/simple_path_int/")
60 | def simple_path_int(sample_variable):
61 | print(sample_variable)
62 | print(type(sample_variable))
63 | print(app.url_map)
64 | return render_template(
65 | 'route_description_tmpl.html',
66 | value=sample_variable,
67 | my_type=type(sample_variable),
68 | my_id=id(sample_variable),
69 | )
70 |
71 | @app.route("/path/")
72 | def path_all(my_path):
73 | print(my_path)
74 | print(type(my_path))
75 | print(app.url_map)
76 | return render_template(
77 | 'route_description_tmpl.html',
78 | value=my_path,
79 | my_type=type(my_path),
80 | my_id=id(my_path),
81 | )
82 |
83 |
84 | @app.route("/person/", methods=['GET', 'POST'])
85 | def person_info(person_name):
86 | if request.method == 'GET':
87 | return get_person_info(person_name)
88 | elif request.method == 'POST':
89 | return post_person_info(person_name)
90 |
91 |
92 | def get_person_info(person_name):
93 | # do samodzielnego zastanowienia się, czy to bezpieczne
94 | person = persons.get(person_name)
95 | return render_template(
96 | 'person_tmpl.html',
97 | name=person.get('name'),
98 | surname=person.get('surname'),
99 | occupation=person.get('occupation'),
100 | )
101 |
102 | def post_person_info(person_name):
103 | data = request.get_json()
104 | new_person = {
105 | 'name': data.get('name'),
106 | 'surname': data.get('surname'),
107 | 'occupation': data.get('occupation')
108 | }
109 | global persons
110 | persons[data.get('name')] = new_person
111 | return 'OK'
112 |
113 |
114 | @app.route("/my_cookies")
115 | def cookies():
116 | cookie_secret = request.cookies.get('cookie_secret')
117 | resp = make_response(
118 | render_template(
119 | 'cookies_tmpl.html', cookie_secret=cookie_secret
120 | )
121 | )
122 | resp.set_cookie('cookie_secret', 'I am cookie')
123 | return resp
124 |
125 |
126 | if __name__ == '__main__':
127 | app.run(debug=True)
128 |
--------------------------------------------------------------------------------
/zajecia_2/praca_domowa/README.MD:
--------------------------------------------------------------------------------
1 | # Praca domowa 2
2 |
3 | Odpowiedź podaj **wyłącznie** przez: https://goo.gl/forms/l51AAbUGAlDVMJAc2
4 |
5 | Na odpowiedzi czekamy do 27.03.2018 23:59:59 czasu polskiego.
6 |
7 | ## Zadanie
8 |
9 | Twój znajomy jest fanatykiem wędkarsktwa. Zlecił Ci przygotowanie portalu na
10 | którym wędkarze mogą dzielić się swoimi zdobyczami.
11 |
12 | Wykonaj endpointy według poniższych wymagań:
13 |
14 | ### `/`
15 | * **GET**
16 | Zwracamy dowolną treść. Ważne aby serwer odpowiadał na tej ścieżce.
17 |
18 | ### `/login`
19 | * **POST**
20 | > Jeżeli macie ochotę, możecie dodać obsługę metody **GET** ułatwi Wam to testowanie aplikacji z poziomu przeglądarki. Sprawdzarka nie weźmie jej pod uwagę.
21 |
22 | Za pomocą mechanizmu BasicAuth pozwala na zalogowanie użytkownika. Poprawne podanie loginu i hasła sprawia, że dany użytkownik będzie miał dostęp do wszystkich pozostałych endpointów w aplikacji. Podpowiedź: :cookie: (a jeszcze lepiej mechanizm `flask.session` - swoją drogą oparty na :cookie:)
23 | Stwórz następującego użytkownika:
24 | * login: `Akwarysta69`
25 | * pass: `J3si07r`.
26 |
27 | Uwaga: Celowo pomijamy rejestrowanie nowego użytkownika
28 |
29 | Jeżeli użytkownik jest już zalogowany, przekieruj na `/hello`.
30 |
31 |
32 | ### `/logout`
33 | Do tego endpointu mają dostęp tylko zalogowani użytkownicy. Jeżeli użytkownik nie jest zalogowany, przekieruj na `/login`.
34 |
35 | * **POST**
36 | Wyloguje aktualnego użytkownika z aplikacji uniemożliwiając mu dostęp do żadnego endpointu poza `/` i `/login`.
37 |
38 | > Jeżeli macie ochotę, możecie dodać obsługę metody **GET** ułatwi Wam to testowanie aplikacji z poziomu przeglądarki. Sprawdzarka nie weźmie jej pod uwagę.
39 |
40 | > *Pamiętaj, aby po wylogowaniu **nie** przekierowywać na `/login`, zamiast tego przekieruj na `/`.*
41 |
42 |
43 | ### `/hello`
44 | Do tego endpointu mają dostęp tylko zalogowani użytkownicy. Jeżeli użytkownik nie jest zalogowany, przekieruj na `/login`.
45 |
46 | * **GET**
47 | Po zalogowaniu użytkownik powinien zostać przekierowany na ten endpoint.
48 | Template niech zawiera element o `"id=greeting"`.
49 | Format powitania: `Hello, {user}!`
50 |
51 | ### `/fishes`
52 | Do tego endpointu mają dostęp tylko zalogowani użytkownicy. Jeżeli użytkownik nie jest zalogowany, przekieruj na `/login`.
53 |
54 | * **GET**
55 | Wylistuje dane wszystkich ryb z ich identyfikatorami. Identyfikatory oczywiście takie jakie im przypisaliście podczas ich tworzenia w metodzie **POST**.
56 | > **Testować będziemy z *query string* (*qs*): `?format=json`. Oczekujemy w odpowiedzi danych w postaci JSON**.
57 |
58 | > Jeżeli macie ochotę, możecie zwrócić ładniej sformatowy template w przypadku braku *qs*.
59 |
60 | ```json
61 | {
62 | "id_1": {
63 | "who": "Znajomy",
64 | "where": {
65 | "lat": 0.001,
66 | "long": 0.002
67 | },
68 | "mass": 34.56,
69 | "length": 23.67,
70 | "kind": "szczupak"
71 | },
72 | "id_2": {
73 | "who": "Kolega kolegi",
74 | "where": {
75 | "lat": 34.001,
76 | "long": 52.002
77 | },
78 | "mass": 300.12,
79 | "length": 234.56,
80 | "kind": "sum olimpijczyk"
81 | }
82 | }
83 | ```
84 |
85 | * **POST**
86 | Doda nową rybkę.
87 | Format: JSON
88 | > Pomyślne dodanie nowej rybki zakończone powinno być przekierowaniem użytkownika na ładniej sformatowany template na której będzie mógł obejrzeć swoją zdobycz: `GET /fishes/?format=json`.
89 |
90 | ```json
91 | {
92 | "who": "Znajomy",
93 | "where": {
94 | "lat": 0.001,
95 | "long": 0.002
96 | },
97 | "mass": 34.56,
98 | "length": 23.67,
99 | "kind": "szczupak"
100 | }
101 | ```
102 |
103 | ### `/fishes/`
104 | Do tego endpointu mają dostęp tylko zalogowani użytkownicy. Jeżeli użytkownik nie jest zalogowany, przekieruj na `/login`.
105 |
106 | * **GET**
107 | Zwraca info danej rybki.
108 | > **Testować będziemy z *query string* (*qs*): `?format=json`. Oczekujemy w odpowiedzi danych w postaci JSON**.
109 |
110 | > Jeżeli macie ochotę, możecie zwrócić ładniej sformatowy template w przypadku braku *qs*.
111 |
112 | * **DELETE**
113 | Usuwa rybkę
114 |
115 | * **PUT**
116 | Podmienia rybkę.
117 |
118 | * **PATCH**
119 | Modyfikuje według podanych wartości, przykład:
120 | Przymując rybkę o takich danych początkowych:
121 | ```json
122 | {
123 | "who": "Znajomy",
124 | "where": {
125 | "lat": 0.001,
126 | "long": 0.002
127 | },
128 | "mass": 34.56,
129 | "length": 23.67,
130 | "kind": "szczupak"
131 | }
132 | ```
133 | Po zawołaniu `PATCHA'a` z takimi danymi:
134 | ```json
135 | {
136 | "who": "Nieznajomy"
137 | }
138 | ```
139 | Wynikowa rybka powinna wyglądać następująco:
140 | ```json
141 | {
142 | "who": "Nieznajomy",
143 | "where": {
144 | "lat": 0.001,
145 | "long": 0.002
146 | },
147 | "mass": 34.56,
148 | "length": 23.67,
149 | "kind": "szczupak"
150 | }
151 | ```
152 | > Zakładamy, że chcemy podmienić wartości kluczy najwyższego poziomu, czyli bazując na powyższym przypadku klucze: `"who", "where", "mass", "length", "kind"`. Jeżeli chcemy zmienić wartość `"lat"`, podobiektu `"where"` podajmy cały podobiekt `"where"`.
153 |
154 |
155 | ## Komentarz
156 | * Na potrzeby tego zadania wystarczy trzymanie danych w wewnętrznych strukturach danych w pamięci.
157 | * Wystarczy 1 worker na heroku. Na zajęciach 4 będziemy się zajmować aplikacjami wieloworkerowymi.
158 | Dla przypomnienia, poniżej podajemy sposób w jaki można skonfigurować `gunicorn'a` na heroku w trybie jednoworkerowym.
159 | ```shell
160 | heroku config:set WEB_CONCURRENCY=1
161 | ```
162 |
163 | * Aplikacja będzie sprawdzana pomiędzy 28.03.2018 07:00 UTC a 30.03 18:00 UTC i w tym oknie czasowym powinna być dostępna.
164 | * W opisie kilku endpointów zamieściliśmy informację o query stringu który będziemy dołączać do zapytań: `?format=json` - nasz automat będzie brał pod uwagę content zwrócony tylko w JSON.
165 | * Więcej info tutaj: [Zadanie domowe 2 - pytania](https://github.com/daftcode/python_levelup_2018/issues/2)
166 |
--------------------------------------------------------------------------------
/zajecia_2/praca_domowa/rozwiazanie/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.python.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | Flask = "*"
8 | gunicorn = "*"
9 |
10 | [dev-packages]
11 |
12 | [requires]
13 | python_version = "3.6"
14 |
--------------------------------------------------------------------------------
/zajecia_2/praca_domowa/rozwiazanie/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "a1f9aa21180c1461a80ff1794860c0f3e45c58ecb6a5fd6f1ef433b28491afc6"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.6"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.python.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {
19 | "click": {
20 | "hashes": [
21 | "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
22 | "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
23 | ],
24 | "version": "==6.7"
25 | },
26 | "flask": {
27 | "hashes": [
28 | "sha256:0749df235e3ff61ac108f69ac178c9770caeaccad2509cb762ce1f65570a8856",
29 | "sha256:49f44461237b69ecd901cc7ce66feea0319b9158743dd27a2899962ab214dac1"
30 | ],
31 | "index": "pypi",
32 | "version": "==0.12.2"
33 | },
34 | "gunicorn": {
35 | "hashes": [
36 | "sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6",
37 | "sha256:eee1169f0ca667be05db3351a0960765620dad53f53434262ff8901b68a1b622"
38 | ],
39 | "index": "pypi",
40 | "version": "==19.7.1"
41 | },
42 | "itsdangerous": {
43 | "hashes": [
44 | "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
45 | ],
46 | "version": "==0.24"
47 | },
48 | "jinja2": {
49 | "hashes": [
50 | "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
51 | "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
52 | ],
53 | "version": "==2.10"
54 | },
55 | "markupsafe": {
56 | "hashes": [
57 | "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
58 | ],
59 | "version": "==1.0"
60 | },
61 | "werkzeug": {
62 | "hashes": [
63 | "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
64 | "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
65 | ],
66 | "version": "==0.14.1"
67 | }
68 | },
69 | "develop": {}
70 | }
71 |
--------------------------------------------------------------------------------
/zajecia_2/praca_domowa/rozwiazanie/Procfile:
--------------------------------------------------------------------------------
1 | web: gunicorn app:app --log-file -
2 |
--------------------------------------------------------------------------------
/zajecia_2/praca_domowa/rozwiazanie/Procfile.local:
--------------------------------------------------------------------------------
1 | web: gunicorn app:app --log-file - --reload
--------------------------------------------------------------------------------
/zajecia_2/praca_domowa/rozwiazanie/app.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 | from uuid import uuid4, UUID
3 |
4 | from flask import Flask, request, Response, session, redirect, url_for, jsonify, \
5 | render_template
6 |
7 | from errors import InvalidUsage
8 |
9 | app = Flask(__name__)
10 | app.secret_key = bytes.fromhex(
11 | 'dfba670ebc21410076bb5941140e789ac6342e09c18da920'
12 | )
13 | app.fishes = {}
14 |
15 |
16 | def get_fish_from_json():
17 | fish_data = request.get_json()
18 | if not fish_data:
19 | raise InvalidUsage('Please provide json data')
20 | return fish_data
21 |
22 |
23 | def set_fish(fish_id=None, data=None, update=False):
24 | if fish_id is None:
25 | fish_id = str(uuid4())
26 |
27 | if data is None:
28 | data = get_fish_from_json()
29 | if data is None:
30 | raise InvalidUsage('Please provide json data')
31 |
32 | if update:
33 | app.fishes[fish_id].update(data)
34 | else:
35 | app.fishes[fish_id] = data
36 |
37 | return fish_id
38 |
39 |
40 | @app.route('/')
41 | def root():
42 | return 'Hello, World!'
43 |
44 |
45 | def check_auth(username, password):
46 | """This function is called to check if a username password combination is
47 | valid."""
48 | return username == 'Akwarysta69' and password == 'J3si07r'
49 |
50 |
51 | def please_authenticate():
52 | """Sends a 401 response that enables basic auth"""
53 | return Response('Could not verify your access level for that URL.\n'
54 | 'You have to login with proper credentials', 401,
55 | {'WWW-Authenticate': 'Basic realm="Login Required"'})
56 |
57 |
58 | def requires_basic_auth(func):
59 | @wraps(func)
60 | def wrapper(*args, **kwargs):
61 | auth = request.authorization
62 | if not auth or not check_auth(auth.username, auth.password):
63 | return please_authenticate()
64 | return func(*args, **kwargs)
65 |
66 | return wrapper
67 |
68 |
69 | @app.route('/login', methods=['GET', 'POST'])
70 | @requires_basic_auth
71 | def login():
72 | session['username'] = request.authorization.username
73 | return redirect(url_for('hello'))
74 |
75 |
76 | def requires_user_session(func):
77 | @wraps(func)
78 | def wrapper(*args, **kwargs):
79 | if not session.get('username'):
80 | return redirect(url_for('login'))
81 | return func(*args, **kwargs)
82 |
83 | return wrapper
84 |
85 |
86 | @app.route('/hello')
87 | @requires_user_session
88 | def hello():
89 | return render_template('greeting.html', name=session['username'])
90 |
91 |
92 | @app.route('/logout', methods=['GET', 'POST'])
93 | @requires_user_session
94 | def logout():
95 | if request.method == 'GET':
96 | return redirect(url_for('root'))
97 | del session['username']
98 | return redirect(url_for('root'))
99 |
100 |
101 | @app.errorhandler(InvalidUsage)
102 | def handle_invalid_usage(error):
103 | response = jsonify(error.to_dict())
104 | response.status_code = error.status_code
105 | return response
106 |
107 |
108 | @app.route('/fishes', methods=['GET', 'POST'])
109 | @requires_user_session
110 | def fishes():
111 | if request.method == 'GET':
112 | return jsonify(app.fishes)
113 | elif request.method == 'POST':
114 | fish_id = set_fish()
115 | return redirect(url_for('fish', fish_id=fish_id, format='json'))
116 |
117 |
118 | @app.route('/fishes/',
119 | methods=['GET', 'PUT', 'PATCH', 'DELETE'])
120 | def fish(fish_id):
121 | if fish_id not in app.fishes:
122 | return 'No such fish', 404
123 |
124 | if request.method == 'DELETE':
125 | del app.fishes[fish_id]
126 | return '', 204
127 |
128 | if request.method == 'PUT':
129 | set_fish(fish_id)
130 | elif request.method == 'PATCH':
131 | set_fish(fish_id, update=True)
132 |
133 | if request.method == 'GET' and request.args.get('format') != 'json':
134 | raise InvalidUsage("Missing 'format=json' in query string.",
135 | status_code=415)
136 | return jsonify(app.fishes[fish_id])
137 |
138 |
139 | if __name__ == '__main__':
140 | app.run(debug=True, use_debugger=False)
141 |
--------------------------------------------------------------------------------
/zajecia_2/praca_domowa/rozwiazanie/errors.py:
--------------------------------------------------------------------------------
1 | class InvalidUsage(Exception):
2 | status_code = 400
3 |
4 | def __init__(self, message, status_code=None, payload=None):
5 | Exception.__init__(self)
6 | self.message = message
7 | if status_code is not None:
8 | self.status_code = status_code
9 | self.payload = payload
10 |
11 | def to_dict(self):
12 | rv = dict(self.payload or ())
13 | rv['message'] = self.message
14 | return rv
15 |
--------------------------------------------------------------------------------
/zajecia_2/praca_domowa/rozwiazanie/templates/greeting.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Aplikacja dla wędkarzy
6 |
7 |
8 | Hello, {{ name }}!
9 |
10 |
--------------------------------------------------------------------------------
/zajecia_2/templates/cookies_tmpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Cookies
5 |
6 |
7 |
8 | Cookie secret: {{ cookie_secret }}
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/zajecia_2/templates/person_tmpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ name }} {{ surname }}
5 |
6 |
7 |
8 | Name: {{ name }}
9 |
10 |
11 | Surname: {{ surname }}
12 |
13 |
14 | Occupation: {{ occupation }}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/zajecia_2/templates/querystring_render_tmpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Wyrenderowane z templatki
5 |
6 |
7 | {% for key, value in query_string_data|dictsort %}
8 | {{ key }}: {{ value }}
9 | {% endfor %}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/zajecia_2/templates/route_description_tmpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Route description
5 |
6 |
7 | value: {{ value }}
8 |
9 | type: {{ my_type }}
10 |
11 | id: {{ my_id }}
12 |
13 |
14 |
--------------------------------------------------------------------------------