├── .gitignore ├── LICENSE ├── README.md ├── ch01 ├── README.md ├── example │ ├── core.py │ ├── run.py │ └── util │ │ ├── __init__.py │ │ ├── db.py │ │ ├── maths.py │ │ └── network.py ├── factorial.py ├── files_only │ ├── core.py │ ├── db.py │ ├── maths.py │ ├── network.py │ └── run.py ├── names.py ├── scopes1.py ├── scopes2.py ├── scopes3.py ├── virtualenv.creation.txt └── virtualenv.example.txt ├── ch02 ├── bytearray.txt ├── chainmap.txt ├── dateandtime.txt ├── defaultdict.txt ├── dicts.txt ├── enum.txt ├── final_considerations.txt ├── lists.txt ├── namedtuple.txt ├── numbers.txt ├── objects.txt ├── requirements │ ├── requirements.in │ └── requirements.txt ├── sequences.txt ├── sets.txt └── tuples.txt ├── ch03 ├── any.py ├── binary.2.py ├── binary.py ├── compress.py ├── conditional.1.py ├── conditional.2.py ├── coupons.dict.py ├── coupons.py ├── discount.py ├── errorsalert.py ├── for.else.py ├── for.no.else.py ├── infinite.py ├── match.py ├── menu.no.walrus.py ├── menu.walrus.py ├── multiple.sequences.enumerate.py ├── multiple.sequences.py ├── multiple.sequences.tuple.py ├── multiple.sequences.unpack.py ├── multiple.sequences.while.py ├── multiple.sequences.zip.py ├── permutations.py ├── primes.else.py ├── primes.py ├── range.txt ├── simple.for.2.py ├── simple.for.3.py ├── simple.for.4.py ├── simple.for.py ├── taxes.py ├── ternary.py └── walrus.if.py ├── ch04 ├── arguments.combined.py ├── arguments.keyword.py ├── arguments.multiple.value.py ├── arguments.positional.keyword.py ├── arguments.positional.py ├── arguments.unpack.dict.py ├── arguments.unpack.iterable.py ├── data.science.example.py ├── docstrings.py ├── filter.lambda.py ├── filter.regular.py ├── func.attributes.py ├── func_from.py ├── func_import.py ├── imports.py ├── key.points.argument.passing.py ├── key.points.assignment.py ├── key.points.mutable.assignment.py ├── key.points.mutable.py ├── lambda.explained.py ├── matrix.multiplication.func.py ├── matrix.multiplication.nofunc.py ├── no.side.effects.py ├── parameters.all.pkwonly.py ├── parameters.all.py ├── parameters.default.py ├── parameters.defaults.mutable.intermediate.call.py ├── parameters.defaults.mutable.no.trap.py ├── parameters.defaults.mutable.py ├── parameters.keyword.only.py ├── parameters.positional.only.optional.py ├── parameters.positional.only.py ├── parameters.variable.db.py ├── parameters.variable.keyword.py ├── parameters.variable.positional.py ├── primes.py ├── recursive.factorial.py ├── return.multiple.py ├── return.none.py ├── return.single.value.2.py ├── return.single.value.py ├── scoping.level.1.py ├── scoping.level.2.global.py ├── scoping.level.2.nonlocal.py ├── scoping.level.2.py ├── util │ ├── __init__.py │ └── funcdef.py ├── vat.function.py └── vat.nofunc.py ├── ch05 ├── decorate.sort.undecorate.py ├── dictionary.comprehensions.duplicates.py ├── dictionary.comprehensions.positions.py ├── dictionary.comprehensions.py ├── even.squares.py ├── fibonacci.elegant.py ├── fibonacci.first.py ├── fibonacci.second.py ├── filter.txt ├── first.n.squares.manual.py ├── first.n.squares.py ├── functions.py ├── gen.filter.py ├── gen.map.filter.py ├── gen.map.py ├── gen.send.preparation.py ├── gen.send.preparation.stop.py ├── gen.send.py ├── gen.yield.for.py ├── gen.yield.from.py ├── gen.yield.return.py ├── generator.expressions.txt ├── list.iterable.txt ├── map.example.txt ├── pairs.for.loop.py ├── pairs.list.comprehension.py ├── performance.map.py ├── performance.py ├── pythagorean.triple.comprehension.py ├── pythagorean.triple.generation.for.py ├── pythagorean.triple.generation.py ├── pythagorean.triple.int.py ├── pythagorean.triple.py ├── pythagorean.triple.walrus.py ├── scopes.for.py ├── scopes.noglobal.py ├── scopes.py ├── set.comprehensions.py ├── squares.comprehension.txt ├── squares.for.txt ├── squares.map.txt ├── squares.py ├── sum.example.2.py ├── sum.example.py ├── zip.grades.txt └── zip.strict.txt ├── ch06 ├── decorators │ ├── decorators.factory.py │ ├── syntax.py │ ├── time.measure.arguments.py │ ├── time.measure.deco1.py │ ├── time.measure.deco2.py │ ├── time.measure.dry.py │ ├── time.measure.start.py │ └── two.decorators.py ├── iterators │ └── iterator.py └── oop │ ├── cached.property.py │ ├── class.attribute.shadowing.py │ ├── class.init.py │ ├── class.issubclass.isinstance.py │ ├── class.methods.factory.py │ ├── class.methods.split.py │ ├── class.namespaces.py │ ├── class.price.py │ ├── class.self.py │ ├── class_inheritance.py │ ├── dataclass.py │ ├── mro.py │ ├── mro.simple.py │ ├── multiple.inheritance.py │ ├── operator.overloading.py │ ├── private.attrs.fixed.py │ ├── private.attrs.py │ ├── property.py │ ├── simplest.class.py │ ├── static.methods.py │ ├── super.duplication.py │ ├── super.explicit.py │ └── super.implicit.py ├── ch07 ├── context │ ├── decimal.prec.ctx.py │ ├── decimal.prec.py │ ├── decimal.prec.try.py │ ├── generator.py │ ├── manager.class.py │ └── multiple.py └── exceptions │ ├── first.example.txt │ ├── for.loop.py │ ├── groups │ ├── exc.group.txt │ ├── handle.group.txt │ ├── handle.nested.txt │ └── util.py │ ├── multiple.py │ ├── note.py │ ├── raising.txt │ ├── replace.txt │ ├── trace.back.py │ ├── try.syntax.py │ └── unhandled.py ├── ch08 ├── config_files │ ├── book-version │ │ ├── config-ini.txt │ │ └── config-toml.txt │ ├── config-ini.txt │ ├── config-toml.txt │ ├── config.ini │ └── config.toml ├── files │ ├── compression │ │ ├── content1.txt │ │ ├── content2.txt │ │ ├── subfolder │ │ │ ├── content3.txt │ │ │ └── content4.txt │ │ ├── tar.py │ │ └── zip.py │ ├── existence.py │ ├── fear.txt │ ├── listing.py │ ├── manipulation.py │ ├── open_try.py │ ├── open_with.py │ ├── ops_create.py │ ├── paths.py │ ├── print_file.py │ ├── read_write.py │ ├── read_write_bin.py │ ├── tmp.py │ ├── walking.pathlib.py │ ├── walking.py │ └── write_not_exists.py ├── io_examples │ ├── reqs.py │ ├── reqs_post.py │ └── string_io.py ├── json_examples │ ├── json_basic.py │ ├── json_cplx.py │ ├── json_datetime.py │ └── json_tuple.py ├── persistence │ ├── alchemy.py │ ├── alchemy_models.py │ ├── pickler.py │ └── shelf.py └── requirements │ ├── requirements.in │ └── requirements.txt ├── ch09 ├── hlib.txt ├── hmc.py ├── jwt │ ├── claims_auth.py │ ├── claims_time.py │ ├── rsa │ │ ├── key │ │ └── key.pub │ ├── tok.py │ └── token_rsa.py ├── requirements │ ├── requirements.in │ └── requirements.txt └── secrs │ ├── secr_gen.py │ ├── secr_rand.py │ └── secr_reset.py ├── ch10 ├── api.py ├── data.py ├── first.run.failure.txt ├── first.run.txt ├── full.run.txt ├── requirements │ ├── requirements.in │ └── requirements.txt └── tests │ ├── __init__.py │ └── test_api.py ├── ch11 ├── assertions.py ├── custom.py ├── custom_timestamp.py ├── log.py ├── pdebugger.py ├── pdebugger_pdb.py ├── profiling │ ├── triples.py │ ├── triples_v2.py │ ├── triples_v3.py │ └── triples_v4.py └── requirements │ ├── requirements.in │ └── requirements.txt ├── ch12 ├── annotations │ ├── any.py │ ├── basic.py │ ├── collections.abc.iterable.py │ ├── collections.abcs.py │ ├── containers.py │ ├── generics.py │ ├── optional.py │ ├── protocols.py │ ├── protocols.subclassing.py │ ├── self.py │ ├── tuples.any.length.py │ ├── tuples.fixed.py │ ├── tuples.named.py │ ├── type.aliases.py │ ├── union.py │ ├── variable.parameters.py │ └── variables.py ├── duck.typing.py ├── example.dynamically.typed.java.txt ├── example.strongly.typed.php ├── example.strongly.typed.py ├── mypy_src │ ├── case.fixed.py │ ├── case.py │ ├── simple_function.py │ └── simple_function_annotated.py └── requirements │ ├── requirements.in │ └── requirements.txt ├── ch13 ├── .gitignore ├── ch13-dataprep.ipynb ├── ch13.ipynb ├── ch13.jupyterlab-workspace ├── data.json ├── example.ipynb └── requirements │ ├── requirements.in │ └── requirements.txt ├── ch14 ├── README.md ├── api_code │ ├── .env.example │ ├── api │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── config.py │ │ ├── crud.py │ │ ├── database.py │ │ ├── deps.py │ │ ├── models.py │ │ ├── schemas.py │ │ ├── stations.py │ │ ├── tickets.py │ │ ├── trains.py │ │ ├── users.py │ │ └── util.py │ ├── dummy_data.py │ ├── main.py │ ├── queries.md │ └── train.db ├── requirements │ ├── dev.in │ ├── dev.txt │ ├── requirements.in │ └── requirements.txt └── samples │ └── api.calls │ ├── stations.txt │ └── users.txt ├── ch15 ├── README.md ├── argument_parsing │ ├── argv.py │ ├── greet.argparse.py │ └── greet.argv.py ├── project │ ├── .env.example │ ├── railway_cli │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── client.py │ │ │ └── schemas.py │ │ ├── cli.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── base.py │ │ │ └── stations.py │ │ ├── config.py │ │ └── exceptions.py │ └── secrets │ │ ├── railway_api_email │ │ └── railway_api_password └── requirements │ ├── dev.in │ ├── dev.txt │ ├── requirements.in │ └── requirements.txt ├── ch16 ├── pip_install.txt ├── pypirc ├── railway-project │ ├── .flake8 │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── pyproject.toml │ ├── src │ │ └── railway_cli │ │ │ ├── __init__.py │ │ │ ├── __main__.py │ │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── client.py │ │ │ └── schemas.py │ │ │ ├── cli.py │ │ │ ├── commands │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── base.py │ │ │ └── stations.py │ │ │ ├── config.py │ │ │ └── exceptions.py │ └── test │ │ ├── __init__.py │ │ └── test_get_station.py ├── requirements │ ├── build.in │ ├── build.txt │ ├── dev.in │ ├── dev.txt │ ├── requirements.in │ └── requirements.txt └── skeleton-project │ ├── README.md │ ├── pyproject.toml │ ├── src │ └── example │ │ └── __init__.py │ └── tests │ └── __init__.py └── ch17 ├── day11.py ├── day7.py ├── input11.txt ├── input7.txt ├── requirements ├── requirements.in └── requirements.txt └── util.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Packt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ch01/README.md: -------------------------------------------------------------------------------- 1 | Chapter 1 data files 2 | ==================== 3 | 4 | The files in this folder are not supposed to work if run. 5 | They serve as source for the book chapters, and to provide a 6 | quick copy/paste tool for whoever would need their content. 7 | -------------------------------------------------------------------------------- /ch01/example/core.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Learn-Python-Programming-Fourth-Edition/980f6176943862553f2165f303789c77e0879eb9/ch01/example/core.py -------------------------------------------------------------------------------- /ch01/example/run.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Learn-Python-Programming-Fourth-Edition/980f6176943862553f2165f303789c77e0879eb9/ch01/example/run.py -------------------------------------------------------------------------------- /ch01/example/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Learn-Python-Programming-Fourth-Edition/980f6176943862553f2165f303789c77e0879eb9/ch01/example/util/__init__.py -------------------------------------------------------------------------------- /ch01/example/util/db.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Learn-Python-Programming-Fourth-Edition/980f6176943862553f2165f303789c77e0879eb9/ch01/example/util/db.py -------------------------------------------------------------------------------- /ch01/example/util/maths.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Learn-Python-Programming-Fourth-Edition/980f6176943862553f2165f303789c77e0879eb9/ch01/example/util/maths.py -------------------------------------------------------------------------------- /ch01/example/util/network.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Learn-Python-Programming-Fourth-Edition/980f6176943862553f2165f303789c77e0879eb9/ch01/example/util/network.py -------------------------------------------------------------------------------- /ch01/factorial.py: -------------------------------------------------------------------------------- 1 | >>> from math import factorial 2 | >>> factorial(5) 3 | 120 -------------------------------------------------------------------------------- /ch01/files_only/core.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Learn-Python-Programming-Fourth-Edition/980f6176943862553f2165f303789c77e0879eb9/ch01/files_only/core.py -------------------------------------------------------------------------------- /ch01/files_only/db.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Learn-Python-Programming-Fourth-Edition/980f6176943862553f2165f303789c77e0879eb9/ch01/files_only/db.py -------------------------------------------------------------------------------- /ch01/files_only/maths.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Learn-Python-Programming-Fourth-Edition/980f6176943862553f2165f303789c77e0879eb9/ch01/files_only/maths.py -------------------------------------------------------------------------------- /ch01/files_only/network.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Learn-Python-Programming-Fourth-Edition/980f6176943862553f2165f303789c77e0879eb9/ch01/files_only/network.py -------------------------------------------------------------------------------- /ch01/files_only/run.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Learn-Python-Programming-Fourth-Edition/980f6176943862553f2165f303789c77e0879eb9/ch01/files_only/run.py -------------------------------------------------------------------------------- /ch01/names.py: -------------------------------------------------------------------------------- 1 | >>> n = 3 # integer number 2 | >>> address = "221b Baker Street, NW1 6XE, London" # Sherlock Holmes' address 3 | >>> employee = { 4 | ... 'age': 45, 5 | ... 'role': 'CTO', 6 | ... 'SSN': 'AB1234567', 7 | ... } 8 | >>> # let us print them 9 | >>> n 10 | 3 11 | >>> address 12 | '221b Baker Street, NW1 6XE, London' 13 | >>> employee 14 | {'age': 45, 'role': 'CTO', 'SSN': 'AB1234567'} 15 | >>> other_name 16 | Traceback (most recent call last): 17 | File "", line 1, in 18 | NameError: name 'other_name' is not defined 19 | >>> 20 | -------------------------------------------------------------------------------- /ch01/scopes1.py: -------------------------------------------------------------------------------- 1 | # scopes1.py 2 | # Local versus Global 3 | 4 | # we define a function, called local 5 | def local(): 6 | age = 7 7 | print(age) 8 | 9 | # we define age within the global scope 10 | age = 5 11 | 12 | # we call, or `execute` the function local 13 | local() 14 | 15 | print(age) 16 | -------------------------------------------------------------------------------- /ch01/scopes2.py: -------------------------------------------------------------------------------- 1 | # scopes2.py 2 | # Local versus Global 3 | 4 | def local(): 5 | # age does not belong to the scope defined by the local 6 | # function so Python will keep looking into the next enclosing 7 | # scope. age is finally found in the global scope. 8 | print(age, 'printing from the local scope') 9 | 10 | age = 5 11 | print(age, 'printing from the global scope') 12 | 13 | local() 14 | -------------------------------------------------------------------------------- /ch01/scopes3.py: -------------------------------------------------------------------------------- 1 | # scopes3.py 2 | # Local, Enclosing and Global 3 | 4 | 5 | def enclosing_func(): 6 | age = 13 7 | 8 | def local(): 9 | # age does not belong to the scope defined by the local 10 | # function so Python will keep looking into the next 11 | # enclosing scope. This time age is found in the enclosing 12 | # scope 13 | print(age, 'printing from the local scope') 14 | 15 | # calling the function local 16 | local() 17 | 18 | age = 5 19 | print(age, 'printing from the global scope') 20 | 21 | enclosing_func() 22 | -------------------------------------------------------------------------------- /ch01/virtualenv.example.txt: -------------------------------------------------------------------------------- 1 | fab@m1:~/code$ mkdir my-project 2 | fab@m1:~/code$ cd my-project 3 | fab@m1:~/code/my-project$ python3.12 -m venv lpp4ed 4 | fab@m1:~/code/my-project$ source ./lpp4ed/bin/activate 5 | (lpp4ed) fab@m1:~/code/my-project$ cat requirements.txt 6 | django==5.0.3 7 | requests==2.31.0 8 | # the following instruction shows how to use pip to install 9 | # requirements from a file 10 | (lpp4ed) fab@m1:~/code/my-project$ pip install -r requirements.txt 11 | Collecting django==5.0.3 (from -r requirements.txt (line 1)) 12 | Using cached Django-5.0.3-py3-none-any.whl.metadata (4.2 kB) 13 | Collecting requests==2.31.0 (from -r requirements.txt (line 2)) 14 | Using cached requests-2.31.0-py3-none-any.whl.metadata (4.6 kB) 15 | ... more collecting omitted ... 16 | Installing collected packages: ..., requests, django 17 | Successfully installed ... django-5.0.3 requests-2.31.0 18 | (lpp4ed) fab@m1:~/code/my-project$ 19 | -------------------------------------------------------------------------------- /ch02/bytearray.txt: -------------------------------------------------------------------------------- 1 | # bytearray.py 2 | 3 | 4 | >>> bytearray() # empty bytearray object 5 | bytearray(b'') 6 | >>> bytearray(10) # zero-filled instance with given length 7 | bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') 8 | >>> bytearray(range(5)) # bytearray from iterable of integers 9 | bytearray(b'\x00\x01\x02\x03\x04') 10 | >>> name = bytearray(b"Lina") #A - bytearray from bytes 11 | >>> name.replace(b"L", b"l") 12 | bytearray(b'lina') 13 | >>> name.endswith(b'na') 14 | True 15 | >>> name.upper() 16 | bytearray(b'LINA') 17 | >>> name.count(b'L') 18 | 1 19 | -------------------------------------------------------------------------------- /ch02/chainmap.txt: -------------------------------------------------------------------------------- 1 | # chainmap.py 2 | 3 | 4 | >>> from collections import ChainMap 5 | >>> default_connection = {'host': 'localhost', 'port': 4567} 6 | >>> connection = {'port': 5678} 7 | >>> conn = ChainMap(connection, default_connection) # map creation 8 | >>> conn['port'] # port is found in the first dictionary 9 | 5678 10 | >>> conn['host'] # host is fetched from the second dictionary 11 | 'localhost' 12 | >>> conn.maps # we can see the mapping objects 13 | [{'port': 5678}, {'host': 'localhost', 'port': 4567}] 14 | >>> conn['host'] = 'packtpub.com' # let's add host 15 | >>> conn.maps 16 | [{'port': 5678, 'host': 'packtpub.com'}, 17 | {'host': 'localhost', 'port': 4567}] 18 | >>> del conn['port'] # let's remove the port information 19 | >>> conn.maps 20 | [{'host': 'packtpub.com'}, {'host': 'localhost', 'port': 4567}] 21 | >>> conn['port'] # now port is fetched from the second dictionary 22 | 4567 23 | >>> dict(conn) # easy to merge and convert to regular dictionary 24 | {'host': 'packtpub.com', 'port': 4567} 25 | -------------------------------------------------------------------------------- /ch02/defaultdict.txt: -------------------------------------------------------------------------------- 1 | # defaultdict.py 2 | 3 | 4 | >>> d = {} 5 | >>> d["age"] = d.get("age", 0) + 1 # age not there, we get 0 + 1 6 | >>> d 7 | {'age': 1} 8 | >>> d = {"age": 39} 9 | >>> d["age"] = d.get("age", 0) + 1 # age is there, we get 40 10 | >>> d 11 | {'age': 40} 12 | 13 | 14 | >>> from collections import defaultdict 15 | >>> dd = defaultdict(int) # int is the default type (0 the value) 16 | >>> dd["age"] += 1 # short for dd['age'] = dd['age'] + 1 17 | >>> dd 18 | defaultdict(, {'age': 1}) # 1, as expected 19 | -------------------------------------------------------------------------------- /ch02/enum.txt: -------------------------------------------------------------------------------- 1 | >>> GREEN = 1 2 | >>> YELLOW = 2 3 | >>> RED = 4 4 | >>> TRAFFIC_LIGHTS = (GREEN, YELLOW, RED) 5 | >>> # or with a dict 6 | >>> traffic_lights = {"GREEN": 1, "YELLOW": 2, "RED": 4} 7 | 8 | 9 | 10 | # using enum 11 | >>> from enum import Enum 12 | >>> class TrafficLight(Enum): 13 | ... GREEN = 1 14 | ... YELLOW = 2 15 | ... RED = 4 16 | ... 17 | >>> TrafficLight.GREEN 18 | 19 | >>> TrafficLight.GREEN.name 20 | 'GREEN' 21 | >>> TrafficLight.GREEN.value 22 | 1 23 | >>> TrafficLight(1) 24 | 25 | >>> TrafficLight(4) 26 | 27 | -------------------------------------------------------------------------------- /ch02/final_considerations.txt: -------------------------------------------------------------------------------- 1 | # final_considerations.py 2 | 3 | 4 | >>> a = 1000000 5 | >>> b = 1000000 6 | >>> id(a) == id(b) 7 | False 8 | 9 | 10 | >>> a = 5 11 | >>> b = 5 12 | >>> id(a) == id(b) 13 | True 14 | 15 | 16 | # how to choose data structures 17 | # example customer objects 18 | customer1 = {"id": "abc123", "full_name": "Master Yoda"} 19 | customer2 = {"id": "def456", "full_name": "Obi-Wan Kenobi"} 20 | customer3 = {"id": "ghi789", "full_name": "Anakin Skywalker"} 21 | # collect them in a tuple 22 | customers = (customer1, customer2, customer3) 23 | # or collect them in a list 24 | customers = [customer1, customer2, customer3] 25 | # or maybe within a dictionary, they have a unique id after all 26 | customers = { 27 | "abc123": customer1, 28 | "def456": customer2, 29 | "ghi789": customer3, 30 | } 31 | 32 | 33 | # negative indexing 34 | >>> a = list(range(10)) # `a` has 10 elements. Last one is 9. 35 | >>> a 36 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 37 | >>> len(a) # its length is 10 elements 38 | 10 39 | >>> a[len(a) - 1] # position of last one is len(a) - 1 40 | 9 41 | >>> a[-1] # but we do not need len(a)! Python rocks! 42 | 9 43 | >>> a[-2] # equivalent to len(a) - 2 44 | 8 45 | >>> a[-3] # equivalent to len(a) - 3 46 | 7 47 | -------------------------------------------------------------------------------- /ch02/namedtuple.txt: -------------------------------------------------------------------------------- 1 | # namedtuple.py 2 | 3 | 4 | # the problem 5 | >>> vision = (9.5, 8.8) 6 | >>> vision 7 | (9.5, 8.8) 8 | >>> vision[0] # left eye (implicit positional reference) 9 | 9.5 10 | >>> vision[1] # right eye (implicit positional reference) 11 | 8.8 12 | 13 | 14 | # the solution 15 | >>> from collections import namedtuple 16 | >>> Vision = namedtuple('Vision', ['left', 'right']) 17 | >>> vision = Vision(9.5, 8.8) 18 | >>> vision[0] 19 | 9.5 20 | >>> vision.left # same as vision[0], but explicit 21 | 9.5 22 | >>> vision.right # same as vision[1], but explicit 23 | 8.8 24 | 25 | 26 | # the change 27 | >>> Vision = namedtuple('Vision', ['left', 'combined', 'right']) 28 | >>> vision = Vision(9.5, 9.2, 8.8) 29 | >>> vision.left # still correct 30 | 9.5 31 | >>> vision.right # still correct (though now is vision[2]) 32 | 8.8 33 | >>> vision.combined # the new vision[1] 34 | 9.2 35 | -------------------------------------------------------------------------------- /ch02/objects.txt: -------------------------------------------------------------------------------- 1 | # objects.py 2 | 3 | # code block # 1 4 | >>> age = 42 5 | >>> age 6 | 42 7 | >>> age = 43 #A 8 | >>> age 9 | 43 10 | 11 | 12 | # code block # 2 13 | >>> age = 42 14 | >>> id(age) 15 | 4377553168 16 | >>> age = 43 17 | >>> id(age) 18 | 4377553200 19 | 20 | 21 | # code block # 3 22 | >>> numbers = set() 23 | >>> id(numbers) 24 | 4368427136 25 | >>> numbers 26 | set() 27 | 28 | >>> numbers.add(3) 29 | >>> numbers.add(7) 30 | >>> id(numbers) 31 | 4368427136 32 | >>> numbers 33 | {3, 7} 34 | -------------------------------------------------------------------------------- /ch02/requirements/requirements.in: -------------------------------------------------------------------------------- 1 | arrow 2 | -------------------------------------------------------------------------------- /ch02/requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | arrow==1.3.0 8 | # via -r requirements.in 9 | python-dateutil==2.9.0.post0 10 | # via arrow 11 | six==1.16.0 12 | # via python-dateutil 13 | types-python-dateutil==2.9.0.20241003 14 | # via arrow 15 | -------------------------------------------------------------------------------- /ch02/tuples.txt: -------------------------------------------------------------------------------- 1 | # tuples.py 2 | 3 | 4 | >>> t = () # empty tuple 5 | >>> type(t) 6 | 7 | >>> one_element_tuple = (42, ) # you need the comma! 8 | >>> three_elements_tuple = (1, 3, 5) # braces are optional here 9 | >>> a, b, c = 1, 2, 3 # tuple for multiple assignment 10 | >>> a, b, c # implicit tuple to print with one instruction 11 | (1, 2, 3) 12 | >>> 3 in three_elements_tuple # membership test 13 | True 14 | 15 | 16 | # swap 17 | >>> a, b = 1, 2 18 | >>> c = a # we need three lines and a temporary var c 19 | >>> a = b 20 | >>> b = c 21 | >>> a, b # a and b have been swapped 22 | (2, 1) 23 | 24 | 25 | # pythonic swap 26 | >>> a, b = 0, 1 27 | >>> a, b = b, a # this is the Pythonic way to do it 28 | >>> a, b 29 | (1, 0) 30 | -------------------------------------------------------------------------------- /ch03/any.py: -------------------------------------------------------------------------------- 1 | # any.py 2 | items = [0, None, 0.0, True, 0, 7] # True and 7 evaluate to True 3 | 4 | found = False # this is called a "flag" 5 | for item in items: 6 | print("scanning item", item) 7 | if item: 8 | found = True # we update the flag 9 | break 10 | 11 | if found: # we inspect the flag 12 | print("At least one item evaluates to True") 13 | else: 14 | print("All items evaluate to False") 15 | -------------------------------------------------------------------------------- /ch03/binary.2.py: -------------------------------------------------------------------------------- 1 | # binary.2.py 2 | n = 39 3 | remainders = [] 4 | while n > 0: 5 | n, remainder = divmod(n, 2) 6 | remainders.append(remainder) 7 | 8 | remainders.reverse() 9 | print(remainders) 10 | -------------------------------------------------------------------------------- /ch03/binary.py: -------------------------------------------------------------------------------- 1 | # binary.py 2 | """ 3 | 6 / 2 = 3 (remainder: 0) 4 | 3 / 2 = 1 (remainder: 1) 5 | 1 / 2 = 0 (remainder: 1) 6 | List of remainders: 0, 1, 1. 7 | Reversed is 1, 1, 0, which is also the binary repres. of 6: 110 8 | """ 9 | 10 | n = 39 11 | remainders = [] 12 | while n > 0: 13 | remainder = n % 2 # remainder of division by 2 14 | remainders.append(remainder) # we keep track of remainders 15 | n //= 2 # we divide n by 2 16 | 17 | remainders.reverse() 18 | print(remainders) 19 | -------------------------------------------------------------------------------- /ch03/compress.py: -------------------------------------------------------------------------------- 1 | # compress.py 2 | from itertools import compress 3 | 4 | 5 | data = range(10) 6 | even_selector = [1, 0] * 10 7 | odd_selector = [0, 1] * 10 8 | 9 | even_numbers = list(compress(data, even_selector)) 10 | odd_numbers = list(compress(data, odd_selector)) 11 | 12 | print(odd_selector) 13 | print(list(data)) 14 | print(even_numbers) 15 | print(odd_numbers) 16 | -------------------------------------------------------------------------------- /ch03/conditional.1.py: -------------------------------------------------------------------------------- 1 | # conditional.1.py 2 | late = True 3 | if late: 4 | print("I need to call my manager!") 5 | -------------------------------------------------------------------------------- /ch03/conditional.2.py: -------------------------------------------------------------------------------- 1 | # conditional.2.py 2 | late = False 3 | if late: 4 | print("I need to call my manager!") # 1 5 | else: 6 | print("no need to call my manager...") # 2 7 | -------------------------------------------------------------------------------- /ch03/coupons.dict.py: -------------------------------------------------------------------------------- 1 | # coupons.dict.py 2 | customers = [ 3 | dict(id=1, total=200, coupon_code="F20"), # F20: fixed, £20 4 | dict(id=2, total=150, coupon_code="P30"), # P30: percent, 30% 5 | dict(id=3, total=100, coupon_code="P50"), # P50: percent, 50% 6 | dict(id=4, total=110, coupon_code="F15"), # F15: fixed, £15 7 | ] 8 | discounts = { 9 | "F20": (0.0, 20.0), # each value is (percent, fixed) 10 | "P30": (0.3, 0.0), 11 | "P50": (0.5, 0.0), 12 | "F15": (0.0, 15.0), 13 | } 14 | for customer in customers: 15 | code = customer["coupon_code"] 16 | percent, fixed = discounts.get(code, (0.0, 0.0)) 17 | customer["discount"] = percent * customer["total"] + fixed 18 | 19 | for customer in customers: 20 | print(customer["id"], customer["total"], customer["discount"]) 21 | -------------------------------------------------------------------------------- /ch03/coupons.py: -------------------------------------------------------------------------------- 1 | # coupons.py 2 | customers = [ 3 | dict(id=1, total=200, coupon_code="F20"), # F20: fixed, £20 4 | dict(id=2, total=150, coupon_code="P30"), # P30: percent, 30% 5 | dict(id=3, total=100, coupon_code="P50"), # P50: percent, 50% 6 | dict(id=4, total=110, coupon_code="F15"), # F15: fixed, £15 7 | ] 8 | for customer in customers: 9 | match customer["coupon_code"]: 10 | case "F20": 11 | customer["discount"] = 20.0 12 | case "F15": 13 | customer["discount"] = 15.0 14 | case "P30": 15 | customer["discount"] = customer["total"] * 0.3 16 | case "P50": 17 | customer["discount"] = customer["total"] * 0.5 18 | case _: 19 | customer["discount"] = 0.0 20 | 21 | for customer in customers: 22 | print(customer["id"], customer["total"], customer["discount"]) 23 | -------------------------------------------------------------------------------- /ch03/discount.py: -------------------------------------------------------------------------------- 1 | # discount.py 2 | from datetime import date, timedelta 3 | 4 | 5 | today = date.today() 6 | tomorrow = today + timedelta(days=1) # today + 1 day is tomorrow 7 | products = [ 8 | {"sku": "1", "expiration_date": today, "price": 100.0}, 9 | {"sku": "2", "expiration_date": tomorrow, "price": 50}, 10 | {"sku": "3", "expiration_date": today, "price": 20}, 11 | ] 12 | 13 | for product in products: 14 | print("Processing sku", product["sku"]) 15 | if product["expiration_date"] != today: 16 | continue 17 | product["price"] *= 0.8 # equivalent to applying 20% discount 18 | print("Sku", product["sku"], "price is now", product["price"]) 19 | -------------------------------------------------------------------------------- /ch03/errorsalert.py: -------------------------------------------------------------------------------- 1 | # errorsalert.py 2 | # The send_email function is defined here to enable you to run 3 | # the code, but of course it doesn't send an actual email. 4 | def send_email(*a): 5 | print(*a) 6 | 7 | 8 | alert_system = "console" # other value can be "email" 9 | error_severity = "critical" # other values: "medium" or "low" 10 | error_message = "Something terrible happened!" 11 | 12 | if alert_system == "console": # outer 13 | print(error_message) # 1 14 | elif alert_system == "email": 15 | if error_severity == "critical": # inner 16 | send_email("admin@example.com", error_message) # 2 17 | elif error_severity == "medium": 18 | send_email("support.1@example.com", error_message) # 3 19 | else: 20 | send_email("support.2@example.com", error_message) # 4 21 | -------------------------------------------------------------------------------- /ch03/for.else.py: -------------------------------------------------------------------------------- 1 | # for.else.py 2 | class DriverException(Exception): 3 | pass 4 | 5 | 6 | people = [("James", 17), ("Kirk", 9), ("Lars", 13), ("Robert", 8)] 7 | for person, age in people: 8 | if age >= 18: 9 | driver = (person, age) 10 | break 11 | else: 12 | raise DriverException("Driver not found.") 13 | -------------------------------------------------------------------------------- /ch03/for.no.else.py: -------------------------------------------------------------------------------- 1 | # for.no.else.py 2 | class DriverException(Exception): 3 | pass 4 | 5 | 6 | people = [("James", 17), ("Kirk", 9), ("Lars", 13), ("Robert", 8)] 7 | driver = None 8 | for person, age in people: 9 | if age >= 18: 10 | driver = (person, age) 11 | break 12 | 13 | if driver is None: 14 | raise DriverException("Driver not found.") 15 | -------------------------------------------------------------------------------- /ch03/infinite.py: -------------------------------------------------------------------------------- 1 | # infinite.py 2 | from itertools import count 3 | 4 | 5 | for n in count(5, 3): 6 | if n > 20: 7 | break 8 | print(n, end=", ") # instead of newline, comma and space 9 | 10 | print() 11 | -------------------------------------------------------------------------------- /ch03/match.py: -------------------------------------------------------------------------------- 1 | # match.py 2 | day_number = 4 3 | 4 | match day_number: 5 | case 1 | 2 | 3 | 4 | 5: 6 | print("Weekday") 7 | case 6: 8 | print("Saturday") 9 | case 7: 10 | print("Sunday") 11 | case _: 12 | print(f"{day_number} is not a valid day number") 13 | -------------------------------------------------------------------------------- /ch03/menu.no.walrus.py: -------------------------------------------------------------------------------- 1 | # menu.no.walrus.py 2 | flavors = ["pistachio", "malaga", "vanilla", "chocolate"] 3 | prompt = "Choose your flavor: " 4 | 5 | print(flavors) 6 | 7 | while True: 8 | choice = input(prompt) 9 | if choice in flavors: 10 | break 11 | 12 | print(f"Sorry, '{choice}' is not a valid option.") 13 | 14 | print(f"You chose '{choice}'.") 15 | -------------------------------------------------------------------------------- /ch03/menu.walrus.py: -------------------------------------------------------------------------------- 1 | # menu.walrus.py 2 | flavors = ["pistachio", "malaga", "vanilla", "chocolate"] 3 | prompt = "Choose your flavor: " 4 | 5 | print(flavors) 6 | 7 | while (choice := input(prompt)) not in flavors: 8 | print(f"Sorry, '{choice}' is not a valid option.") 9 | 10 | print(f"You chose '{choice}'.") 11 | -------------------------------------------------------------------------------- /ch03/multiple.sequences.enumerate.py: -------------------------------------------------------------------------------- 1 | # multiple.sequences.enumerate.py 2 | people = ["Nick", "Rick", "Roger", "Syd"] 3 | ages = [23, 24, 23, 21] 4 | for position, person in enumerate(people): 5 | age = ages[position] 6 | print(person, age) 7 | -------------------------------------------------------------------------------- /ch03/multiple.sequences.py: -------------------------------------------------------------------------------- 1 | # multiple.sequences.py 2 | people = ["Nick", "Rick", "Roger", "Syd"] 3 | ages = [23, 24, 23, 21] 4 | for position in range(len(people)): 5 | person = people[position] 6 | age = ages[position] 7 | print(person, age) 8 | -------------------------------------------------------------------------------- /ch03/multiple.sequences.tuple.py: -------------------------------------------------------------------------------- 1 | # multiple.sequences.tuple.py 2 | people = ["Nick", "Rick", "Roger", "Syd"] 3 | ages = [23, 24, 23, 21] 4 | instruments = ["Drums", "Keyboards", "Bass", "Guitar"] 5 | for data in zip(people, ages, instruments): 6 | print(data) 7 | -------------------------------------------------------------------------------- /ch03/multiple.sequences.unpack.py: -------------------------------------------------------------------------------- 1 | # multiple.sequences.unpack.py 2 | people = ["Nick", "Rick", "Roger", "Syd"] 3 | ages = [23, 24, 23, 21] 4 | instruments = ["Drums", "Keyboards", "Bass", "Guitar"] 5 | for person, age, instrument in zip(people, ages, instruments): 6 | print(person, age, instrument) 7 | -------------------------------------------------------------------------------- /ch03/multiple.sequences.while.py: -------------------------------------------------------------------------------- 1 | # multiple.sequences.while.py 2 | people = ["Nick", "Rick", "Roger", "Syd"] 3 | ages = [23, 24, 23, 21] 4 | position = 0 5 | while position < len(people): 6 | person = people[position] 7 | age = ages[position] 8 | print(person, age) 9 | position += 1 10 | -------------------------------------------------------------------------------- /ch03/multiple.sequences.zip.py: -------------------------------------------------------------------------------- 1 | # multiple.sequences.zip.py 2 | people = ["Nick", "Rick", "Roger", "Syd"] 3 | ages = [23, 24, 23, 21] 4 | for person, age in zip(people, ages): 5 | print(person, age) 6 | -------------------------------------------------------------------------------- /ch03/permutations.py: -------------------------------------------------------------------------------- 1 | # permutations.py 2 | from itertools import permutations 3 | 4 | 5 | print(list(permutations("ABC"))) 6 | -------------------------------------------------------------------------------- /ch03/primes.else.py: -------------------------------------------------------------------------------- 1 | # primes.else.py 2 | primes = [] 3 | upto = 100 4 | for n in range(2, upto + 1): 5 | for divisor in range(2, n): 6 | if n % divisor == 0: 7 | break 8 | else: 9 | primes.append(n) 10 | 11 | print(primes) 12 | -------------------------------------------------------------------------------- /ch03/primes.py: -------------------------------------------------------------------------------- 1 | # primes.py 2 | primes = [] # this will contain the primes in the end 3 | upto = 100 # the limit, inclusive 4 | for n in range(2, upto + 1): 5 | is_prime = True # flag, new at each iteration of outer for 6 | for divisor in range(2, n): 7 | if n % divisor == 0: 8 | is_prime = False 9 | break 10 | if is_prime: # check on flag 11 | primes.append(n) 12 | 13 | print(primes) 14 | -------------------------------------------------------------------------------- /ch03/range.txt: -------------------------------------------------------------------------------- 1 | >>> list(range(10)) # one value: from 0 to value (excluded) 2 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 3 | >>> list(range(3, 8)) # two values: from start to stop (excluded) 4 | [3, 4, 5, 6, 7] 5 | >>> list(range(-10, 10, 4)) # three values: step is added 6 | [-10, -6, -2, 2, 6] 7 | -------------------------------------------------------------------------------- /ch03/simple.for.2.py: -------------------------------------------------------------------------------- 1 | # simple.for.2.py 2 | surnames = ["Rivest", "Shamir", "Adleman"] 3 | for position in range(len(surnames)): 4 | print(position, surnames[position]) 5 | # print(surnames[position][0], end='') # try swapping prints 6 | -------------------------------------------------------------------------------- /ch03/simple.for.3.py: -------------------------------------------------------------------------------- 1 | # simple.for.3.py 2 | surnames = ["Rivest", "Shamir", "Adleman"] 3 | for surname in surnames: 4 | print(surname) 5 | -------------------------------------------------------------------------------- /ch03/simple.for.4.py: -------------------------------------------------------------------------------- 1 | # simple.for.4.py 2 | surnames = ["Rivest", "Shamir", "Adleman"] 3 | for position, surname in enumerate(surnames): 4 | print(position, surname) 5 | -------------------------------------------------------------------------------- /ch03/simple.for.py: -------------------------------------------------------------------------------- 1 | # simple.for.py 2 | for number in [0, 1, 2, 3, 4]: 3 | print(number) 4 | 5 | 6 | # equivalent using range 7 | for number in range(5): 8 | print(number) 9 | -------------------------------------------------------------------------------- /ch03/taxes.py: -------------------------------------------------------------------------------- 1 | # taxes.py 2 | income = 15000 3 | if income < 10000: 4 | tax_coefficient = 0.0 # 1 5 | elif income < 30000: 6 | tax_coefficient = 0.2 # 2 7 | elif income < 100000: 8 | tax_coefficient = 0.35 # 3 9 | else: 10 | tax_coefficient = 0.45 # 4 11 | 12 | print(f"You will pay: ${income * tax_coefficient} in taxes") 13 | -------------------------------------------------------------------------------- /ch03/ternary.py: -------------------------------------------------------------------------------- 1 | # ternary.py 2 | order_total = 247 # GBP 3 | 4 | # classic if/else form 5 | if order_total > 100: 6 | discount = 25 # GBP 7 | else: 8 | discount = 0 # GBP 9 | print(order_total, discount) 10 | 11 | # ternary operator 12 | discount = 25 if order_total > 100 else 0 13 | print(order_total, discount) 14 | -------------------------------------------------------------------------------- /ch03/walrus.if.py: -------------------------------------------------------------------------------- 1 | # walrus.if.py 2 | value = 42 3 | modulus = 11 4 | 5 | # Without assignment expression: 6 | remainder = value % modulus 7 | if remainder: 8 | print(f"Not divisible! The remainder is {remainder}.") 9 | 10 | 11 | # Rewritten with assignment expression: 12 | if remainder := value % modulus: 13 | print(f"Not divisible! The remainder is {remainder}.") 14 | -------------------------------------------------------------------------------- /ch04/arguments.combined.py: -------------------------------------------------------------------------------- 1 | # arguments.combined.py 2 | def func(a, b, c, d, e, f): 3 | print(a, b, c, d, e, f) 4 | 5 | 6 | func(1, *(2, 3), f=6, *(4, 5)) 7 | func(*(1, 2), e=5, *(3, 4), f=6) 8 | func(1, **{"b": 2, "c": 3}, d=4, **{"e": 5, "f": 6}) 9 | func(c=3, *(1, 2), **{"d": 4}, e=5, **{"f": 6}) 10 | -------------------------------------------------------------------------------- /ch04/arguments.keyword.py: -------------------------------------------------------------------------------- 1 | # arguments.keyword.py 2 | def func(a, b, c): 3 | print(a, b, c) 4 | 5 | 6 | func(a=1, c=2, b=3) # prints: 1 3 2 7 | -------------------------------------------------------------------------------- /ch04/arguments.multiple.value.py: -------------------------------------------------------------------------------- 1 | # arguments.multiple.value.py 2 | def func(a, b, c): 3 | print(a, b, c) 4 | 5 | 6 | func(2, 3, a=1) 7 | 8 | """ 9 | $ python arguments.multiple.value.py 10 | Traceback (most recent call last): 11 | File "arguments.multiple.value.py", line 5, in 12 | func(2, 3, a=1) 13 | TypeError: func() got multiple values for argument 'a' 14 | """ 15 | -------------------------------------------------------------------------------- /ch04/arguments.positional.keyword.py: -------------------------------------------------------------------------------- 1 | # arguments.positional.keyword.py 2 | def func(a, b, c): 3 | print(a, b, c) 4 | 5 | func(42, b=1, c=2) 6 | 7 | func(b=1, c=2, 42) # positional arg after keyword args 8 | 9 | """ 10 | $ python arguments.positional.keyword.py 11 | File "arguments.positional.keyword.py", line 7 12 | func(b=1, c=2, 42) # positional arg after keyword args 13 | ^ 14 | SyntaxError: positional argument follows keyword argument 15 | """ 16 | -------------------------------------------------------------------------------- /ch04/arguments.positional.py: -------------------------------------------------------------------------------- 1 | # arguments.positional.py 2 | def func(a, b, c): 3 | print(a, b, c) 4 | 5 | 6 | func(1, 2, 3) # prints: 1 2 3 7 | -------------------------------------------------------------------------------- /ch04/arguments.unpack.dict.py: -------------------------------------------------------------------------------- 1 | # arguments.unpack.dict.py 2 | def func(a, b, c): 3 | print(a, b, c) 4 | 5 | 6 | values = {"b": 1, "c": 2, "a": 42} 7 | func(**values) # equivalent to func(b=1, c=2, a=42) 8 | -------------------------------------------------------------------------------- /ch04/arguments.unpack.iterable.py: -------------------------------------------------------------------------------- 1 | # arguments.unpack.iterable.py 2 | def func(a, b, c): 3 | print(a, b, c) 4 | 5 | 6 | values = (1, 3, -7) 7 | func(*values) # equivalent to: func(1, 3, -7) 8 | -------------------------------------------------------------------------------- /ch04/data.science.example.py: -------------------------------------------------------------------------------- 1 | # data.science.example.py 2 | def do_report(data_source): 3 | # fetch and prepare data 4 | data = fetch_data(data_source) 5 | parsed_data = parse_data(data) 6 | filtered_data = filter_data(parsed_data) 7 | polished_data = polish_data(filtered_data) 8 | 9 | # run algorithms on data 10 | final_data = analyse(polished_data) 11 | 12 | # create and return report 13 | report = Report(final_data) 14 | return report 15 | 16 | 17 | if __name__ == "__main__": 18 | 19 | print( 20 | "Please don't call the `do_report` function. " 21 | "It's just and example." 22 | ) 23 | -------------------------------------------------------------------------------- /ch04/docstrings.py: -------------------------------------------------------------------------------- 1 | # docstrings.py 2 | def square(n): 3 | """Return the square of a number n.""" 4 | return n**2 5 | 6 | 7 | def get_username(userid): 8 | """Return the username of a user given their id.""" 9 | return db.get(user_id=userid).username 10 | 11 | 12 | def connect(host, port, user, password): 13 | """Connect to a database. 14 | 15 | Connect to a PostgreSQL database directly, using the given 16 | parameters. 17 | 18 | :param host: The host IP. 19 | :param port: The desired port. 20 | :param user: The connection username. 21 | :param password: The connection password. 22 | :return: The connection object. 23 | """ 24 | # body of the function here... 25 | return connection 26 | -------------------------------------------------------------------------------- /ch04/filter.lambda.py: -------------------------------------------------------------------------------- 1 | # filter.lambda.py 2 | def get_multiples_of_five(n): 3 | return list(filter(lambda k: not k % 5, range(n))) 4 | 5 | 6 | print(get_multiples_of_five(30)) # [0, 5, 10, 15, 20, 25] 7 | -------------------------------------------------------------------------------- /ch04/filter.regular.py: -------------------------------------------------------------------------------- 1 | # filter.regular.py 2 | def is_multiple_of_five(n): 3 | return not n % 5 4 | 5 | 6 | def get_multiples_of_five(n): 7 | return list(filter(is_multiple_of_five, range(n))) 8 | 9 | 10 | print(get_multiples_of_five(30)) # [0, 5, 10, 15, 20, 25] 11 | -------------------------------------------------------------------------------- /ch04/func_from.py: -------------------------------------------------------------------------------- 1 | # func_from.py 2 | from util.funcdef import square, cube 3 | 4 | print(square(10)) 5 | print(cube(10)) 6 | -------------------------------------------------------------------------------- /ch04/func_import.py: -------------------------------------------------------------------------------- 1 | # func_import.py 2 | import util.funcdef 3 | 4 | 5 | print(util.funcdef.square(10)) 6 | print(util.funcdef.cube(10)) 7 | -------------------------------------------------------------------------------- /ch04/imports.py: -------------------------------------------------------------------------------- 1 | # imports.py 2 | from datetime import datetime, timezone # two imports, same line 3 | from unittest.mock import patch # single import 4 | 5 | import pytest # third party library 6 | 7 | from core.models import ( # multiline import 8 | Exam, 9 | Exercise, 10 | Solution, 11 | ) 12 | -------------------------------------------------------------------------------- /ch04/key.points.argument.passing.py: -------------------------------------------------------------------------------- 1 | # key.points.argument.passing.py 2 | x = 3 3 | 4 | 5 | def func(y): 6 | print(y) 7 | 8 | 9 | func(x) # prints: 3 10 | -------------------------------------------------------------------------------- /ch04/key.points.assignment.py: -------------------------------------------------------------------------------- 1 | # key.points.assignment.py 2 | x = 3 3 | 4 | 5 | def func(x): 6 | x = 7 # defining a local x, not changing the global one 7 | 8 | 9 | func(x) 10 | print(x) # prints: 3 11 | -------------------------------------------------------------------------------- /ch04/key.points.mutable.assignment.py: -------------------------------------------------------------------------------- 1 | # key.points.mutable.assignment.py 2 | x = [1, 2, 3] 3 | 4 | 5 | def func(x): 6 | x[1] = 42 # this changes the original `x` argument! 7 | x = "something else" # this points x to a new string object 8 | 9 | 10 | func(x) 11 | print(x) # still prints: [1, 42, 3] 12 | -------------------------------------------------------------------------------- /ch04/key.points.mutable.py: -------------------------------------------------------------------------------- 1 | # key.points.mutable.py 2 | x = [1, 2, 3] 3 | 4 | 5 | def func(x): 6 | x[1] = 42 # this affects the `x` argument! 7 | 8 | 9 | func(x) 10 | print(x) # prints: [1, 42, 3] 11 | -------------------------------------------------------------------------------- /ch04/lambda.explained.py: -------------------------------------------------------------------------------- 1 | # lambda.explained.py 2 | """ 3 | myfunc = lambda [parameter_list]: expression 4 | 5 | def myfunc([parameter_list]): 6 | return expression 7 | """ 8 | 9 | 10 | # example 1: adder 11 | def adder(a, b): 12 | return a + b 13 | 14 | 15 | # is equivalent to: 16 | adder_lambda = lambda a, b: a + b 17 | 18 | 19 | # example 2: to uppercase 20 | def to_upper(s): 21 | return s.upper() 22 | 23 | 24 | # is equivalent to: 25 | to_upper_lambda = lambda s: s.upper() 26 | 27 | 28 | if __name__ == "__main__": 29 | 30 | print(adder(3, 4)) 31 | print(adder_lambda(3, 4)) 32 | 33 | print(to_upper("Hello")) 34 | print(to_upper_lambda("Hello")) 35 | -------------------------------------------------------------------------------- /ch04/matrix.multiplication.func.py: -------------------------------------------------------------------------------- 1 | # matrix.multiplication.func.py 2 | # this function could also be defined in another module 3 | def matrix_mul(a, b): 4 | return [ 5 | [sum(i * j for i, j in zip(r, c)) for c in zip(*b)] 6 | for r in a 7 | ] 8 | 9 | 10 | a = [[1, 2], [3, 4]] 11 | b = [[5, 1], [2, 1]] 12 | 13 | c = matrix_mul(a, b) 14 | print(c) # [[9, 3], [23, 7]] 15 | -------------------------------------------------------------------------------- /ch04/matrix.multiplication.nofunc.py: -------------------------------------------------------------------------------- 1 | # matrix.multiplication.nofunc.py 2 | a = [[1, 2], [3, 4]] 3 | b = [[5, 1], [2, 1]] 4 | 5 | c = [ 6 | [sum(i * j for i, j in zip(r, c)) for c in zip(*b)] for r in a 7 | ] 8 | print(c) # [[9, 3], [23, 7]] 9 | -------------------------------------------------------------------------------- /ch04/no.side.effects.py: -------------------------------------------------------------------------------- 1 | # no.side.effects.py 2 | # This file is not meant to be run 3 | >>> numbers = [4, 1, 7, 5] 4 | >>> sorted(numbers) # won't sort the original `numbers` list 5 | [1, 4, 5, 7] 6 | >>> numbers # let's verify 7 | [4, 1, 7, 5] # good, untouched 8 | >>> numbers.sort() # this will act on the list 9 | >>> numbers 10 | [1, 4, 5, 7] 11 | -------------------------------------------------------------------------------- /ch04/parameters.all.pkwonly.py: -------------------------------------------------------------------------------- 1 | # parameters.all.pkwonly.py 2 | def allparams(a, /, b, c=42, *args, d=256, e, **kwargs): 3 | print("a, b, c:", a, b, c) 4 | print("d, e:", d, e) 5 | print("args:", args) 6 | print("kwargs:", kwargs) 7 | 8 | 9 | allparams(1, 2, 3, 4, 5, 6, e=7, f=9, g=10) 10 | -------------------------------------------------------------------------------- /ch04/parameters.all.py: -------------------------------------------------------------------------------- 1 | # parameters.all.py 2 | def func(a, b, c=7, *args, **kwargs): 3 | print("a, b, c:", a, b, c) 4 | print("args:", args) 5 | print("kwargs:", kwargs) 6 | 7 | 8 | func(1, 2, 3, 5, 7, 9, A="a", B="b") 9 | -------------------------------------------------------------------------------- /ch04/parameters.default.py: -------------------------------------------------------------------------------- 1 | # parameters.default.py 2 | def func(a, b=4, c=88): 3 | print(a, b, c) 4 | 5 | 6 | func(1) # prints: 1 4 88 7 | func(b=5, a=7, c=9) # prints: 7 5 9 8 | func(42, c=9) # prints: 42 4 9 9 | func(42, 43, 44) # prints: 42, 43, 44 10 | -------------------------------------------------------------------------------- /ch04/parameters.defaults.mutable.intermediate.call.py: -------------------------------------------------------------------------------- 1 | # parameters.defaults.mutable.intermediate.call.py 2 | def func(a=[], b={}): 3 | print(a) 4 | print(b) 5 | print("#" * 12) 6 | a.append(len(a)) # this will affect a's default value 7 | b[len(a)] = len(a) # and this will affect b's one 8 | 9 | 10 | func() 11 | func(a=[1, 2, 3], b={"B": 1}) 12 | func() 13 | -------------------------------------------------------------------------------- /ch04/parameters.defaults.mutable.no.trap.py: -------------------------------------------------------------------------------- 1 | # parameters.defaults.mutable.no.trap.py 2 | def func(a=None): 3 | if a is None: 4 | a = [] 5 | # do whatever you want with `a` ... 6 | -------------------------------------------------------------------------------- /ch04/parameters.defaults.mutable.py: -------------------------------------------------------------------------------- 1 | # parameters.defaults.mutable.py 2 | def func(a=[], b={}): 3 | print(a) 4 | print(b) 5 | print("#" * 12) 6 | a.append(len(a)) # this will affect a's default value 7 | b[len(a)] = len(a) # and this will affect b's one 8 | 9 | 10 | func() 11 | func() 12 | func() 13 | -------------------------------------------------------------------------------- /ch04/parameters.keyword.only.py: -------------------------------------------------------------------------------- 1 | # parameters.keyword.only.py 2 | def kwo(*a, c): 3 | print(a, c) 4 | 5 | 6 | kwo(1, 2, 3, c=7) # prints: (1, 2, 3) 7 7 | kwo(c=4) # prints: () 4 8 | # kwo(1, 2) # breaks, invalid syntax, with the following error 9 | # TypeError: kwo() missing 1 required keyword-only argument: 'c' 10 | 11 | 12 | def kwo2(a, b=42, *, c): 13 | print(a, b, c) 14 | 15 | 16 | kwo2(3, b=7, c=99) # prints: 3 7 99 17 | kwo2(3, c=13) # prints: 3 42 13 18 | # kwo2(3, 23) # breaks, invalid syntax, with the following error 19 | # TypeError: kwo2() missing 1 required keyword-only argument: 'c' 20 | -------------------------------------------------------------------------------- /ch04/parameters.positional.only.optional.py: -------------------------------------------------------------------------------- 1 | # parameters.positional.only.optional.py 2 | def func(a, b=2, /): 3 | print(a, b) 4 | 5 | 6 | func(4, 5) # prints 4 5 7 | func(3) # prints 3 2 8 | -------------------------------------------------------------------------------- /ch04/parameters.positional.only.py: -------------------------------------------------------------------------------- 1 | # parameters.positional.only.py 2 | def func(a, b, /, c): 3 | print(a, b, c) 4 | 5 | 6 | func(1, 2, 3) # prints: 1 2 3 7 | func(1, 2, c=3) # prints 1 2 3 8 | # func(1, b=2, c=3) # produces the following traceback: 9 | """ 10 | Traceback (most recent call last): 11 | File "arguments.positional.only.py", line 7, in 12 | func(1, b=2, c=3) 13 | TypeError: func() got some positional-only arguments 14 | passed as keyword arguments: 'b' 15 | """ 16 | 17 | """ 18 | def func_name(positional_only_parameters, /, 19 | positional_or_keyword_parameters, *, 20 | keyword_only_parameters): 21 | 22 | 23 | Valid: 24 | 25 | def func_name(p1, p2, /, p_or_kw, *, kw): 26 | def func_name(p1, p2=None, /, p_or_kw=None, *, kw): 27 | def func_name(p1, p2=None, /, *, kw): 28 | def func_name(p1, p2=None, /): 29 | def func_name(p1, p2, /, p_or_kw): 30 | def func_name(p1, p2, /): 31 | 32 | 33 | Invalid: 34 | 35 | def func_name(p1, p2=None, /, p_or_kw, *, kw): 36 | def func_name(p1=None, p2, /, p_or_kw=None, *, kw): 37 | def func_name(p1=None, p2, /): 38 | 39 | 40 | Why it is useful: 41 | 42 | def divmod(a, b, /): 43 | "Emulate the built in divmod() function" 44 | return (a // b, a % b) 45 | 46 | 47 | len(obj='hello') # The "obj" keyword argument impairs readability 48 | """ 49 | 50 | 51 | def func_name(name, /, **kwargs): 52 | print(name) 53 | print(kwargs) 54 | 55 | 56 | func_name("Positional-only name", name="Name in **kwargs") 57 | 58 | # Prints: 59 | # Positional-only name 60 | # {'name': 'Name in **kwargs'} 61 | -------------------------------------------------------------------------------- /ch04/parameters.variable.db.py: -------------------------------------------------------------------------------- 1 | # parameters.variable.db.py 2 | def connect(**options): 3 | conn_params = { 4 | "host": options.get("host", "127.0.0.1"), 5 | "port": options.get("port", 5432), 6 | "user": options.get("user", ""), 7 | "pwd": options.get("pwd", ""), 8 | } 9 | print(conn_params) 10 | # we then connect to the db (commented out) 11 | # db.connect(**conn_params) 12 | 13 | 14 | connect() 15 | connect(host="127.0.0.42", port=5433) 16 | connect(port=5431, user="fab", pwd="gandalf") 17 | -------------------------------------------------------------------------------- /ch04/parameters.variable.keyword.py: -------------------------------------------------------------------------------- 1 | # parameters.variable.keyword.py 2 | def func(**kwargs): 3 | print(kwargs) 4 | 5 | 6 | func(a=1, b=42) # prints {'a': 1, 'b': 42} 7 | func() # prints {} 8 | func(a=1, b=46, c=99) # prints {'a': 1, 'b': 46, 'c': 99} 9 | -------------------------------------------------------------------------------- /ch04/parameters.variable.positional.py: -------------------------------------------------------------------------------- 1 | # parameters.variable.positional.py 2 | def minimum(*n): 3 | # print(type(n)) # n is a tuple 4 | if n: # explained after the code 5 | mn = n[0] 6 | for value in n[1:]: 7 | if value < mn: 8 | mn = value 9 | print(mn) 10 | 11 | 12 | minimum(1, 3, -7, 9) # n = (1, 3, -7, 9) - prints: -7 13 | minimum() # n = () - prints: nothing 14 | -------------------------------------------------------------------------------- /ch04/recursive.factorial.py: -------------------------------------------------------------------------------- 1 | # recursive.factorial.py 2 | def factorial(n): 3 | if n in (0, 1): # base case 4 | return 1 5 | return factorial(n - 1) * n # recursive case 6 | 7 | 8 | print([factorial(n) for n in range(10)]) 9 | -------------------------------------------------------------------------------- /ch04/return.multiple.py: -------------------------------------------------------------------------------- 1 | # return.multiple.py 2 | def moddiv(a, b): 3 | return a // b, a % b 4 | 5 | 6 | def test(n=10**4): 7 | from random import choice, random, randint 8 | 9 | m = 10**6 10 | for x in range(n): 11 | a = choice((randint(0, m), m * random())) 12 | b = 0 13 | while not b: 14 | b = choice((randint(1, m), m * random())) 15 | 16 | r = divmod(a, b) 17 | r2 = moddiv(a, b) 18 | if r != r2: 19 | print("Difference: ", a, b, r, r2) 20 | 21 | 22 | if __name__ == "__main__": 23 | 24 | test(10**6) 25 | print("Done") 26 | 27 | print(moddiv(20, 7)) # prints (2, 6) 28 | -------------------------------------------------------------------------------- /ch04/return.none.py: -------------------------------------------------------------------------------- 1 | # return.none.py 2 | def func(): 3 | pass 4 | 5 | 6 | func() # the return of this call won't be collected. It's lost. 7 | a = func() # the return of this one instead is collected into `a` 8 | print(a) # prints: None 9 | -------------------------------------------------------------------------------- /ch04/return.single.value.2.py: -------------------------------------------------------------------------------- 1 | # return.single.value.2.py 2 | from functools import reduce 3 | from operator import mul 4 | 5 | 6 | def factorial(n): 7 | return reduce(mul, range(1, n + 1), 1) 8 | 9 | 10 | f5 = factorial(5) # f5 = 120 11 | print(f5) 12 | print([factorial(k) for k in range(10)]) 13 | -------------------------------------------------------------------------------- /ch04/return.single.value.py: -------------------------------------------------------------------------------- 1 | # return.single.value.py 2 | def factorial(n): 3 | if n in (0, 1): 4 | return 1 5 | result = n 6 | for k in range(2, n): 7 | result *= k 8 | return result 9 | 10 | 11 | f5 = factorial(5) # f5 = 120 12 | print(f5) 13 | -------------------------------------------------------------------------------- /ch04/scoping.level.1.py: -------------------------------------------------------------------------------- 1 | # scoping.level.1.py 2 | def my_function(): 3 | test = 1 # this is defined in the local scope of the function 4 | print("my_function:", test) 5 | 6 | 7 | test = 0 # this is defined in the global scope 8 | my_function() 9 | print("global:", test) 10 | -------------------------------------------------------------------------------- /ch04/scoping.level.2.global.py: -------------------------------------------------------------------------------- 1 | # scoping.level.2.global.py 2 | def outer(): 3 | test = 1 # outer scope 4 | 5 | def inner(): 6 | global test 7 | test = 2 # global scope 8 | print("inner:", test) 9 | 10 | inner() 11 | print("outer:", test) 12 | 13 | 14 | test = 0 # global scope 15 | outer() 16 | print("global:", test) 17 | -------------------------------------------------------------------------------- /ch04/scoping.level.2.nonlocal.py: -------------------------------------------------------------------------------- 1 | # scoping.level.2.nonlocal.py 2 | def outer(): 3 | test = 1 # outer scope 4 | 5 | def inner(): 6 | nonlocal test 7 | test = 2 # nearest enclosing scope (which is 'outer') 8 | print("inner:", test) 9 | 10 | inner() 11 | print("outer:", test) 12 | 13 | 14 | test = 0 # global scope 15 | outer() 16 | print("global:", test) 17 | -------------------------------------------------------------------------------- /ch04/scoping.level.2.py: -------------------------------------------------------------------------------- 1 | # scoping.level.2.py 2 | def outer(): 3 | test = 1 # outer scope 4 | 5 | def inner(): 6 | test = 2 # inner scope 7 | print("inner:", test) 8 | 9 | inner() 10 | print("outer:", test) 11 | 12 | 13 | test = 0 # global scope 14 | outer() 15 | print("global:", test) 16 | -------------------------------------------------------------------------------- /ch04/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Learn-Python-Programming-Fourth-Edition/980f6176943862553f2165f303789c77e0879eb9/ch04/util/__init__.py -------------------------------------------------------------------------------- /ch04/util/funcdef.py: -------------------------------------------------------------------------------- 1 | # lib/funcdef.py 2 | def square(n): 3 | return n**2 4 | 5 | 6 | def cube(n): 7 | return n**3 8 | -------------------------------------------------------------------------------- /ch04/vat.function.py: -------------------------------------------------------------------------------- 1 | # vat.function.py 2 | def calculate_price_with_vat(price, vat): 3 | return price * (100 + vat) / 100 4 | 5 | 6 | if __name__ == "__main__": 7 | print(calculate_price_with_vat(100, 20)) 8 | -------------------------------------------------------------------------------- /ch04/vat.nofunc.py: -------------------------------------------------------------------------------- 1 | # vat.nofunc.py 2 | price = 100 # GBP, no VAT 3 | final_price1 = price * 1.2 4 | final_price2 = price + price / 5.0 5 | final_price3 = price * (100 + 20) / 100.0 6 | final_price4 = price + price * 0.2 7 | 8 | 9 | if __name__ == "__main__": 10 | for var, value in sorted(vars().items()): 11 | if "price" in var: 12 | print(var, value) 13 | -------------------------------------------------------------------------------- /ch05/decorate.sort.undecorate.py: -------------------------------------------------------------------------------- 1 | # decorate.sort.undecorate.py 2 | from pprint import pprint 3 | 4 | 5 | students = [ 6 | dict(id=0, credits=dict(math=9, physics=6, history=7)), 7 | dict(id=1, credits=dict(math=6, physics=7, latin=10)), 8 | dict(id=2, credits=dict(history=8, physics=9, chemistry=10)), 9 | dict(id=3, credits=dict(math=5, physics=5, geography=7)), 10 | ] 11 | 12 | 13 | def decorate(student): 14 | # create a 2-tuple (sum of credits, student) from student dict 15 | return (sum(student["credits"].values()), student) 16 | 17 | 18 | 19 | def undecorate(decorated_student): 20 | # discard sum of credits, return original student dict 21 | return decorated_student[1] 22 | 23 | 24 | print(students[0]) 25 | print(decorate(students[0])) 26 | 27 | students = sorted(map(decorate, students), reverse=True) 28 | students = list(map(undecorate, students)) 29 | pprint(students) 30 | -------------------------------------------------------------------------------- /ch05/dictionary.comprehensions.duplicates.py: -------------------------------------------------------------------------------- 1 | # dictionary.comprehensions.duplicates.py 2 | word = "Hello" 3 | 4 | swaps = {c: c.swapcase() for c in word} 5 | 6 | print(swaps) # prints: {'H': 'h', 'e': 'E', 'l': 'L', 'o': 'O'} 7 | -------------------------------------------------------------------------------- /ch05/dictionary.comprehensions.positions.py: -------------------------------------------------------------------------------- 1 | # dictionary.comprehensions.positions.py 2 | word = "Hello" 3 | 4 | positions = {c: k for k, c in enumerate(word)} 5 | 6 | print(positions) # prints: {'H': 0, 'e': 1, 'l': 3, 'o': 4} 7 | -------------------------------------------------------------------------------- /ch05/dictionary.comprehensions.py: -------------------------------------------------------------------------------- 1 | # dictionary.comprehensions.py 2 | from string import ascii_lowercase 3 | 4 | 5 | lettermap = {c: k for k, c in enumerate(ascii_lowercase, 1)} 6 | # lettermap = dict((c, k) for k, c in enumerate(ascii_lowercase, 1)) 7 | 8 | 9 | print(lettermap) 10 | -------------------------------------------------------------------------------- /ch05/even.squares.py: -------------------------------------------------------------------------------- 1 | # even.squares.py 2 | # using map and filter 3 | sq1 = list( 4 | map(lambda n: n**2, filter(lambda n: not n % 2, range(10))) 5 | ) 6 | 7 | # equivalent, but using list comprehensions 8 | sq2 = [n**2 for n in range(10) if not n % 2] 9 | 10 | print(sq1, sq1 == sq2) # prints: [0, 4, 16, 36, 64] True 11 | -------------------------------------------------------------------------------- /ch05/fibonacci.elegant.py: -------------------------------------------------------------------------------- 1 | # fibonacci.elegant.py 2 | def fibonacci(N): 3 | """Return all fibonacci numbers up to N.""" 4 | a, b = 0, 1 5 | while a <= N: 6 | yield a 7 | a, b = b, a + b 8 | 9 | 10 | print(list(fibonacci(0))) # [0] 11 | print(list(fibonacci(1))) # [0, 1, 1] 12 | print(list(fibonacci(50))) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] 13 | -------------------------------------------------------------------------------- /ch05/fibonacci.first.py: -------------------------------------------------------------------------------- 1 | # fibonacci.first.py 2 | def fibonacci(N): 3 | """Return all fibonacci numbers up to N.""" 4 | result = [0] 5 | next_n = 1 6 | while next_n <= N: 7 | result.append(next_n) 8 | next_n = sum(result[-2:]) 9 | return result 10 | 11 | 12 | print(fibonacci(0)) # [0] 13 | print(fibonacci(1)) # [0, 1, 1] 14 | print(fibonacci(50)) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] 15 | -------------------------------------------------------------------------------- /ch05/fibonacci.second.py: -------------------------------------------------------------------------------- 1 | # fibonacci.second.py 2 | def fibonacci(N): 3 | """Return all fibonacci numbers up to N.""" 4 | yield 0 5 | if N == 0: 6 | return 7 | a = 0 8 | b = 1 9 | while b <= N: 10 | yield b 11 | a, b = b, a + b 12 | 13 | 14 | print(list(fibonacci(0))) # [0] 15 | print(list(fibonacci(1))) # [0, 1, 1] 16 | print(list(fibonacci(50))) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] 17 | -------------------------------------------------------------------------------- /ch05/filter.txt: -------------------------------------------------------------------------------- 1 | # filter.txt 2 | 3 | >>> test = [2, 5, 8, 0, 0, 1, 0] 4 | >>> list(filter(None, test)) 5 | [2, 5, 8, 1] 6 | >>> list(filter(lambda x: x, test)) # equivalent to previous one 7 | [2, 5, 8, 1] 8 | >>> list(filter(lambda x: x > 4, test)) # keep only items > 4 9 | [5, 8] 10 | -------------------------------------------------------------------------------- /ch05/first.n.squares.manual.py: -------------------------------------------------------------------------------- 1 | # first.n.squares.manual.py 2 | def get_squares_gen(n): 3 | for x in range(n): 4 | yield x**2 5 | 6 | 7 | squares = get_squares_gen(4) # this creates a generator object 8 | print(squares) # 9 | print(next(squares)) # prints: 0 10 | print(next(squares)) # prints: 1 11 | print(next(squares)) # prints: 4 12 | print(next(squares)) # prints: 9 13 | # the following raises StopIteration, the generator is exhausted, 14 | # any further call to next will keep raising StopIteration 15 | print(next(squares)) 16 | -------------------------------------------------------------------------------- /ch05/first.n.squares.py: -------------------------------------------------------------------------------- 1 | # first.n.squares.py 2 | def get_squares(n): # classic function approach 3 | return [x**2 for x in range(n)] 4 | 5 | 6 | print(get_squares(10)) 7 | 8 | 9 | def get_squares_gen(n): # generator approach 10 | for x in range(n): 11 | yield x**2 # we yield, we do not return 12 | 13 | 14 | print(list(get_squares_gen(10))) 15 | -------------------------------------------------------------------------------- /ch05/functions.py: -------------------------------------------------------------------------------- 1 | # functions.py 2 | 3 | 4 | def gcd(a, b): 5 | """Calculate the Greatest Common Divisor of (a, b).""" 6 | while b != 0: 7 | a, b = b, a % b 8 | return a 9 | -------------------------------------------------------------------------------- /ch05/gen.filter.py: -------------------------------------------------------------------------------- 1 | # gen.filter.py 2 | cubes = [x**3 for x in range(10)] 3 | 4 | odd_cubes1 = filter(lambda cube: cube % 2, cubes) 5 | odd_cubes2 = (cube for cube in cubes if cube % 2) 6 | -------------------------------------------------------------------------------- /ch05/gen.map.filter.py: -------------------------------------------------------------------------------- 1 | # gen.map.filter.py 2 | # finds the cubes of all multiples of 3 or 5 below N 3 | N = 20 4 | 5 | cubes1 = map( 6 | lambda n: (n, n**3), 7 | filter(lambda n: n % 3 == 0 or n % 5 == 0, range(N)), 8 | ) 9 | 10 | cubes2 = ((n, n**3) for n in range(N) if n % 3 == 0 or n % 5 == 0) 11 | 12 | print(list(cubes1)) 13 | print(list(cubes2)) 14 | -------------------------------------------------------------------------------- /ch05/gen.map.py: -------------------------------------------------------------------------------- 1 | # gen.map.py 2 | def adder(*n): 3 | return sum(n) 4 | 5 | 6 | s1 = sum(map(adder, range(100), range(1, 101))) 7 | s2 = sum(adder(*n) for n in zip(range(100), range(1, 101))) 8 | -------------------------------------------------------------------------------- /ch05/gen.send.preparation.py: -------------------------------------------------------------------------------- 1 | # gen.send.preparation.py 2 | def counter(start=0): 3 | n = start 4 | while True: 5 | yield n 6 | n += 1 7 | 8 | 9 | c = counter() 10 | print(next(c)) # prints: 0 11 | print(next(c)) # prints: 1 12 | print(next(c)) # prints: 2 13 | -------------------------------------------------------------------------------- /ch05/gen.send.preparation.stop.py: -------------------------------------------------------------------------------- 1 | # gen.send.preparation.stop.py 2 | stop = False 3 | 4 | 5 | def counter(start=0): 6 | n = start 7 | while not stop: 8 | yield n 9 | n += 1 10 | 11 | 12 | c = counter() 13 | print(next(c)) # prints: 0 14 | print(next(c)) # prints: 1 15 | stop = True 16 | print(next(c)) # raises StopIteration 17 | -------------------------------------------------------------------------------- /ch05/gen.send.py: -------------------------------------------------------------------------------- 1 | # gen.send.py 2 | def counter(start=0): 3 | n = start 4 | while True: 5 | result = yield n # A 6 | print(type(result), result) # B 7 | if result == "Q": 8 | break 9 | n += 1 10 | 11 | 12 | c = counter() 13 | print(next(c)) # C 14 | print(c.send("Wow!")) # D 15 | print(next(c)) # E 16 | print(c.send("Q")) # F 17 | -------------------------------------------------------------------------------- /ch05/gen.yield.for.py: -------------------------------------------------------------------------------- 1 | # gen.yield.for.py 2 | def print_squares(start, end): 3 | for n in range(start, end): 4 | yield n**2 5 | 6 | 7 | for n in print_squares(2, 5): 8 | print(n) 9 | -------------------------------------------------------------------------------- /ch05/gen.yield.from.py: -------------------------------------------------------------------------------- 1 | # gen.yield.from.py 2 | def print_squares(start, end): 3 | yield from (n**2 for n in range(start, end)) 4 | 5 | 6 | for n in print_squares(2, 5): 7 | print(n) 8 | -------------------------------------------------------------------------------- /ch05/gen.yield.return.py: -------------------------------------------------------------------------------- 1 | # gen.yield.return.py 2 | 3 | 4 | def geometric_progression(a, q): 5 | k = 0 6 | while True: 7 | result = a * q**k 8 | if result <= 100000: 9 | yield result 10 | else: 11 | return 12 | k += 1 13 | 14 | 15 | for n in geometric_progression(2, 5): 16 | print(n) 17 | -------------------------------------------------------------------------------- /ch05/generator.expressions.txt: -------------------------------------------------------------------------------- 1 | # generator.expressions.txt 2 | 3 | >>> cubes = [k**3 for k in range(10)] # regular list 4 | >>> cubes 5 | [0, 1, 8, 27, 64, 125, 216, 343, 512, 729] 6 | >>> type(cubes) 7 | 8 | >>> cubes_gen = (k**3 for k in range(10)) # create as generator 9 | >>> cubes_gen 10 | at 0x7f08b2004860> 11 | >>> type(cubes_gen) 12 | 13 | >>> list(cubes_gen) # this will exhaust the generator 14 | [0, 1, 8, 27, 64, 125, 216, 343, 512, 729] 15 | >>> list(cubes_gen) # nothing more to give 16 | [] 17 | -------------------------------------------------------------------------------- /ch05/list.iterable.txt: -------------------------------------------------------------------------------- 1 | # list.iterable.txt 2 | 3 | >>> range(7) 4 | range(0, 7) 5 | >>> list(range(7)) # put all elements in a list to view them 6 | [0, 1, 2, 3, 4, 5, 6] 7 | -------------------------------------------------------------------------------- /ch05/map.example.txt: -------------------------------------------------------------------------------- 1 | # map.example.txt 2 | 3 | >>> map(lambda *a: a, range(3)) # 1 iterable 4 | # Not useful! Let us use list 5 | >>> list(map(lambda *a: a, range(3))) # 1 iterable 6 | [(0,), (1,), (2,)] 7 | >>> list(map(lambda *a: a, range(3), "abc")) # 2 iterables 8 | [(0, 'a'), (1, 'b'), (2, 'c')] 9 | >>> list(map(lambda *a: a, range(3), "abc", range(4, 7))) # 3 10 | [(0, 'a', 4), (1, 'b', 5), (2, 'c', 6)] 11 | >>> # map stops at the shortest iterator 12 | >>> list(map(lambda *a: a, (), "abc")) # empty tuple is shortest 13 | [] 14 | >>> list(map(lambda *a: a, (1, 2), "abc")) # (1, 2) shortest 15 | [(1, 'a'), (2, 'b')] 16 | >>> list(map(lambda *a: a, (1, 2, 3, 4), "abc")) # "abc" shortest 17 | [(1, 'a'), (2, 'b'), (3, 'c')] 18 | -------------------------------------------------------------------------------- /ch05/pairs.for.loop.py: -------------------------------------------------------------------------------- 1 | # pairs.for.loop.py 2 | 3 | items = "ABCD" 4 | pairs = [] 5 | 6 | for a in range(len(items)): 7 | for b in range(a, len(items)): 8 | pairs.append((items[a], items[b])) 9 | 10 | print(pairs) 11 | -------------------------------------------------------------------------------- /ch05/pairs.list.comprehension.py: -------------------------------------------------------------------------------- 1 | # pairs.list.comprehension.py 2 | 3 | items = "ABCD" 4 | pairs = [ 5 | (items[a], items[b]) 6 | for a in range(len(items)) 7 | for b in range(a, len(items)) 8 | ] 9 | 10 | print(pairs) 11 | -------------------------------------------------------------------------------- /ch05/performance.map.py: -------------------------------------------------------------------------------- 1 | # performance.map.py 2 | from time import time 3 | 4 | 5 | mx = 2 * 10**7 6 | 7 | t = time() 8 | absloop = [] 9 | for n in range(mx): 10 | absloop.append(abs(n)) 11 | print("for loop: {:.4f} s".format(time() - t)) 12 | 13 | t = time() 14 | abslist = [abs(n) for n in range(mx)] 15 | print("list comprehension: {:.4f} s".format(time() - t)) 16 | 17 | t = time() 18 | absmap = list(map(abs, range(mx))) 19 | print("map: {:.4f} s".format(time() - t)) 20 | 21 | print(absloop == abslist == absmap) 22 | -------------------------------------------------------------------------------- /ch05/performance.py: -------------------------------------------------------------------------------- 1 | # performance.py 2 | from time import time 3 | 4 | 5 | mx = 5000 6 | 7 | t = time() # start time for the for loop 8 | floop = [] 9 | for a in range(1, mx): 10 | for b in range(a, mx): 11 | floop.append(divmod(a, b)) 12 | print("for loop: {:.4f} s".format(time() - t)) # elapsed time 13 | 14 | 15 | t = time() # start time for the list comprehension 16 | compr = [divmod(a, b) for a in range(1, mx) for b in range(a, mx)] 17 | print("list comprehension: {:.4f} s".format(time() - t)) 18 | 19 | 20 | t = time() # start time for the generator expression 21 | gener = list( 22 | divmod(a, b) for a in range(1, mx) for b in range(a, mx) 23 | ) 24 | print("generator expression: {:.4f} s".format(time() - t)) 25 | 26 | 27 | # verify correctness of results and number of items in each list 28 | print(floop == compr == gener, len(floop), len(compr), len(gener)) 29 | -------------------------------------------------------------------------------- /ch05/pythagorean.triple.comprehension.py: -------------------------------------------------------------------------------- 1 | # pythagorean.triple.comprehension.py 2 | from math import sqrt 3 | 4 | 5 | # this step is the same as before 6 | mx = 10 7 | triples = [ 8 | (a, b, sqrt(a**2 + b**2)) 9 | for a in range(1, mx) 10 | for b in range(a, mx) 11 | ] 12 | # here we combine filter and map in one CLEAN list comprehension 13 | triples = [ 14 | (a, b, int(c)) for a, b, c in triples if c.is_integer() 15 | ] 16 | 17 | print(triples) # prints: [(3, 4, 5), (6, 8, 10)] 18 | -------------------------------------------------------------------------------- /ch05/pythagorean.triple.generation.for.py: -------------------------------------------------------------------------------- 1 | # pythagorean.triple.generation.for.py 2 | from functions import gcd 3 | 4 | 5 | def gen_triples(N): 6 | for m in range(1, int(N**0.5) + 1): # 1 7 | for n in range(1, m): # 2 8 | if (m - n) % 2 and gcd(m, n) == 1: # 3 9 | c = m**2 + n**2 # 4 10 | if c <= N: # 5 11 | a = m**2 - n**2 # 6 12 | b = 2 * m * n # 7 13 | yield (a, b, c) # 8 14 | 15 | 16 | triples = sorted(gen_triples(50), key=sum) # 9 17 | print(triples) 18 | -------------------------------------------------------------------------------- /ch05/pythagorean.triple.generation.py: -------------------------------------------------------------------------------- 1 | # pythagorean.triple.generation.py 2 | from functions import gcd 3 | 4 | 5 | N = 50 6 | 7 | triples = sorted( # 1 8 | ( 9 | (a, b, c) 10 | for a, b, c in ( # 2 11 | ((m**2 - n**2), (2 * m * n), (m**2 + n**2)) # 3 12 | for m in range(1, int(N**0.5) + 1) # 4 13 | for n in range(1, m) # 5 14 | if (m - n) % 2 and gcd(m, n) == 1 # 6 15 | ) 16 | if c <= N # 7 17 | ), 18 | key=sum, # 8 19 | ) 20 | 21 | 22 | print(triples) 23 | -------------------------------------------------------------------------------- /ch05/pythagorean.triple.int.py: -------------------------------------------------------------------------------- 1 | # pythagorean.triple.int.py 2 | from math import sqrt 3 | 4 | 5 | mx = 10 6 | triples = [ 7 | (a, b, sqrt(a**2 + b**2)) 8 | for a in range(1, mx) 9 | for b in range(a, mx) 10 | ] 11 | triples = filter(lambda triple: triple[2].is_integer(), triples) 12 | 13 | # this will make the third number in the tuples integer 14 | triples = list( 15 | map(lambda triple: triple[:2] + (int(triple[2]),), triples) 16 | ) 17 | 18 | print(triples) # prints: [(3, 4, 5), (6, 8, 10)] 19 | -------------------------------------------------------------------------------- /ch05/pythagorean.triple.py: -------------------------------------------------------------------------------- 1 | # pythagorean.triple.py 2 | from math import sqrt 3 | 4 | 5 | # this will generate all possible pairs 6 | mx = 10 7 | triples = [ 8 | (a, b, sqrt(a**2 + b**2)) 9 | for a in range(1, mx) 10 | for b in range(a, mx) 11 | ] 12 | # this will filter out all non pythagorean triples 13 | triples = list( 14 | filter(lambda triple: triple[2].is_integer(), triples) 15 | ) 16 | 17 | print(triples) # prints: [(3, 4, 5.0), (6, 8, 10.0)] 18 | -------------------------------------------------------------------------------- /ch05/pythagorean.triple.walrus.py: -------------------------------------------------------------------------------- 1 | # pythagorean.triple.walrus.py 2 | from math import sqrt 3 | 4 | 5 | # this step is the same as before 6 | mx = 10 7 | # We can combine generating and filtering in one comprehension 8 | triples = [ 9 | (a, b, int(c)) 10 | for a in range(1, mx) 11 | for b in range(a, mx) 12 | if (c := sqrt(a**2 + b**2)).is_integer() 13 | ] 14 | 15 | print(triples) # prints: [(3, 4, 5), (6, 8, 10)] 16 | -------------------------------------------------------------------------------- /ch05/scopes.for.py: -------------------------------------------------------------------------------- 1 | # scopes.for.py 2 | s = 0 3 | for A in range(5): 4 | s += A 5 | print(A) # prints: 4 6 | print(globals()) 7 | -------------------------------------------------------------------------------- /ch05/scopes.noglobal.py: -------------------------------------------------------------------------------- 1 | # scopes.noglobal.py 2 | ex1 = [A for A in range(5)] 3 | print(A) # breaks: NameError: name 'A' is not defined 4 | -------------------------------------------------------------------------------- /ch05/scopes.py: -------------------------------------------------------------------------------- 1 | # scopes.py 2 | A = 100 3 | ex1 = [A for A in range(5)] 4 | print(A) # prints: 100 5 | 6 | ex2 = list(A for A in range(5)) 7 | print(A) # prints: 100 8 | 9 | ex3 = {A: 2 * A for A in range(5)} 10 | print(A) # prints: 100 11 | 12 | ex4 = {A for A in range(5)} 13 | print(A) # prints: 100 14 | 15 | s = 0 16 | for A in range(5): 17 | s += A 18 | print(A) # prints: 4 19 | -------------------------------------------------------------------------------- /ch05/set.comprehensions.py: -------------------------------------------------------------------------------- 1 | # set.comprehensions.py 2 | word = "Hello" 3 | 4 | letters1 = {c for c in word} 5 | letters2 = set(c for c in word) 6 | print(letters1) # prints: {'H', 'o', 'e', 'l'} 7 | print(letters1 == letters2) # prints: True 8 | -------------------------------------------------------------------------------- /ch05/squares.comprehension.txt: -------------------------------------------------------------------------------- 1 | # squares.comprehension.txt 2 | 3 | >>> [n ** 2 for n in range(10)] 4 | [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] 5 | -------------------------------------------------------------------------------- /ch05/squares.for.txt: -------------------------------------------------------------------------------- 1 | # squares.for.txt 2 | 3 | >>> squares = [] 4 | >>> for n in range(10): 5 | ... squares.append(n ** 2) 6 | ... 7 | >>> squares 8 | [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] 9 | -------------------------------------------------------------------------------- /ch05/squares.map.txt: -------------------------------------------------------------------------------- 1 | # squares.map.txt 2 | 3 | >>> squares = list(map(lambda n: n**2, range(10))) 4 | >>> squares 5 | [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] 6 | -------------------------------------------------------------------------------- /ch05/squares.py: -------------------------------------------------------------------------------- 1 | # squares.py 2 | def square1(n): 3 | return n**2 # squaring through the power operator 4 | 5 | 6 | def square2(n): 7 | return n * n # squaring through multiplication 8 | -------------------------------------------------------------------------------- /ch05/sum.example.2.py: -------------------------------------------------------------------------------- 1 | # sum.example.2.py 2 | # s = sum([n**2 for n in range(10**10)]) # this is killed 3 | s = sum(n**2 for n in range(10**10)) # this succeeds 4 | 5 | print(s) # prints: 333333333283333333335000000000 6 | -------------------------------------------------------------------------------- /ch05/sum.example.py: -------------------------------------------------------------------------------- 1 | # sum.example.py 2 | s1 = sum([n**2 for n in range(10**6)]) 3 | s2 = sum((n**2 for n in range(10**6))) 4 | s3 = sum(n**2 for n in range(10**6)) 5 | 6 | print(s1 == s2 == s3) 7 | -------------------------------------------------------------------------------- /ch05/zip.grades.txt: -------------------------------------------------------------------------------- 1 | # zip.grades.txt 2 | 3 | >>> grades = [18, 23, 30, 27] 4 | >>> avgs = [22, 21, 29, 24] 5 | >>> list(zip(avgs, grades)) 6 | [(22, 18), (21, 23), (29, 30), (24, 27)] 7 | >>> list(map(lambda *a: a, avgs, grades)) # equivalent to zip 8 | [(22, 18), (21, 23), (29, 30), (24, 27)] 9 | -------------------------------------------------------------------------------- /ch05/zip.strict.txt: -------------------------------------------------------------------------------- 1 | # zip.strict.txt 2 | 3 | >>> students = ["Sophie", "Alex", "Charlie", "Alice"] 4 | >>> grades = ["A", "C", "B"] 5 | >>> dict(zip(students, grades)) 6 | {'Sophie': 'A', 'Alex': 'C', 'Charlie': 'B'} 7 | >>> dict(zip(students, grades, strict=True)) 8 | Traceback (most recent call last): 9 | File "", line 1, in 10 | ValueError: zip() argument 2 is shorter than argument 1 11 | -------------------------------------------------------------------------------- /ch06/decorators/decorators.factory.py: -------------------------------------------------------------------------------- 1 | # decorators/decorators.factory.py 2 | from functools import wraps 3 | 4 | 5 | def max_result(threshold): 6 | def decorator(func): 7 | @wraps(func) 8 | def wrapper(*args, **kwargs): 9 | result = func(*args, **kwargs) 10 | if result > threshold: 11 | print( 12 | f"Result is too big ({result})." 13 | f"Max allowed is {threshold}." 14 | ) 15 | return result 16 | return wrapper 17 | return decorator 18 | 19 | 20 | @max_result(75) 21 | def cube(n): 22 | return n**3 23 | 24 | 25 | @max_result(100) 26 | def square(n): 27 | return n**2 28 | 29 | 30 | @max_result(1000) 31 | def multiply(a, b): 32 | return a * b 33 | 34 | 35 | print(cube(5)) 36 | 37 | 38 | """ 39 | $ python decorators.factory.py 40 | Result is too big (125). Max allowed is 75. 41 | 125 42 | """ 43 | -------------------------------------------------------------------------------- /ch06/decorators/syntax.py: -------------------------------------------------------------------------------- 1 | # decorators/syntax.py 2 | # This is not a valid Python module - Don't run it. 3 | 4 | # ONE DECORATOR 5 | def func(arg1, arg2, ...): 6 | pass 7 | 8 | func = decorator(func) 9 | 10 | # is equivalent to the following: 11 | 12 | @decorator 13 | def func(arg1, arg2, ...): 14 | pass 15 | 16 | 17 | # TWO DECORATORS 18 | def func(arg1, arg2, ...): 19 | pass 20 | 21 | func = deco1(deco2(func)) 22 | 23 | # is equivalent to the following: 24 | 25 | @deco1 26 | @deco2 27 | def func(arg1, arg2, ...): 28 | pass 29 | 30 | # DECORATOR WITH ARGUMENTS 31 | def func(arg1, arg2, ...): 32 | pass 33 | 34 | func = decoarg(arg_a, arg_b)(func) 35 | 36 | # is equivalent to the following: 37 | 38 | @decoarg(arg_a, arg_b) 39 | def func(arg1, arg2, ...): 40 | pass 41 | -------------------------------------------------------------------------------- /ch06/decorators/time.measure.arguments.py: -------------------------------------------------------------------------------- 1 | # decorators/time.measure.arguments.py 2 | from time import sleep, time 3 | 4 | 5 | def f(sleep_time=0.1): 6 | sleep(sleep_time) 7 | 8 | 9 | def measure(func, *args, **kwargs): 10 | t = time() 11 | func(*args, **kwargs) 12 | print(func.__name__, "took:", time() - t) 13 | 14 | 15 | measure(f, sleep_time=0.3) # f took: 0.30092811584472656 16 | measure(f, 0.2) # f took: 0.20505475997924805 17 | -------------------------------------------------------------------------------- /ch06/decorators/time.measure.deco1.py: -------------------------------------------------------------------------------- 1 | # decorators/time.measure.deco1.py 2 | from time import sleep, time 3 | 4 | 5 | def f(sleep_time=0.1): 6 | sleep(sleep_time) 7 | 8 | 9 | def measure(func): 10 | def wrapper(*args, **kwargs): 11 | t = time() 12 | func(*args, **kwargs) 13 | print(func.__name__, "took:", time() - t) 14 | 15 | return wrapper 16 | 17 | 18 | f = measure(f) # decoration point 19 | 20 | f(0.2) # f took: 0.20128178596496582 21 | f(sleep_time=0.3) # f took: 0.30509519577026367 22 | print(f.__name__) # wrapper <- ouch! 23 | -------------------------------------------------------------------------------- /ch06/decorators/time.measure.deco2.py: -------------------------------------------------------------------------------- 1 | # decorators/time.measure.deco2.py 2 | from time import sleep, time 3 | from functools import wraps 4 | 5 | 6 | def measure(func): 7 | @wraps(func) 8 | def wrapper(*args, **kwargs): 9 | t = time() 10 | func(*args, **kwargs) 11 | print(func.__name__, "took:", time() - t) 12 | 13 | return wrapper 14 | 15 | 16 | @measure 17 | def f(sleep_time=0.1): 18 | """I'm a cat. I love to sleep!""" 19 | sleep(sleep_time) 20 | 21 | 22 | f(sleep_time=0.3) # f took: 0.30042004585266113 23 | print(f.__name__) # f 24 | print(f.__doc__ ) # I'm a cat. I love to sleep! 25 | -------------------------------------------------------------------------------- /ch06/decorators/time.measure.dry.py: -------------------------------------------------------------------------------- 1 | # decorators/time.measure.dry.py 2 | from time import sleep, time 3 | 4 | 5 | def f(): 6 | sleep(0.3) 7 | 8 | 9 | def g(): 10 | sleep(0.5) 11 | 12 | 13 | def measure(func): 14 | t = time() 15 | func() 16 | print(func.__name__, "took:", time() - t) 17 | 18 | 19 | measure(f) # f took: 0.3043971061706543 20 | measure(g) # g took: 0.5050859451293945 21 | -------------------------------------------------------------------------------- /ch06/decorators/time.measure.start.py: -------------------------------------------------------------------------------- 1 | # decorators/time.measure.start.py 2 | from time import sleep, time 3 | 4 | 5 | def f(): 6 | sleep(0.3) 7 | 8 | 9 | def g(): 10 | sleep(0.5) 11 | 12 | 13 | t = time() 14 | f() 15 | print("f took:", time() - t) # f took: 0.3028988838195801 16 | 17 | t = time() 18 | g() 19 | print("g took:", time() - t) # g took: 0.507941722869873 20 | -------------------------------------------------------------------------------- /ch06/decorators/two.decorators.py: -------------------------------------------------------------------------------- 1 | # decorators/two.decorators.py 2 | from time import time 3 | from functools import wraps 4 | 5 | 6 | def measure(func): 7 | @wraps(func) 8 | def wrapper(*args, **kwargs): 9 | t = time() 10 | result = func(*args, **kwargs) 11 | print(func.__name__, "took:", time() - t) 12 | return result 13 | 14 | return wrapper 15 | 16 | 17 | def max_result(func): 18 | @wraps(func) 19 | def wrapper(*args, **kwargs): 20 | result = func(*args, **kwargs) 21 | if result > 100: 22 | print( 23 | f"Result is too big ({result}). " 24 | "Max allowed is 100." 25 | ) 26 | return result 27 | 28 | return wrapper 29 | 30 | 31 | @measure 32 | @max_result 33 | def cube(n): 34 | return n**3 35 | 36 | 37 | print(cube(2)) 38 | print(cube(5)) 39 | 40 | 41 | """ 42 | $ python two.decorators.py 43 | cube took: 9.5367431640625e-07 44 | 8 45 | Result is too big (125). Max allowed is 100. 46 | cube took: 3.0994415283203125e-06 47 | 125 48 | """ 49 | -------------------------------------------------------------------------------- /ch06/iterators/iterator.py: -------------------------------------------------------------------------------- 1 | # iterators/iterator.py 2 | class OddEven: 3 | def __init__(self, data): 4 | self._data = data 5 | self.indexes = list(range(0, len(data), 2)) + list( 6 | range(1, len(data), 2) 7 | ) 8 | 9 | def __iter__(self): 10 | return self 11 | 12 | def __next__(self): 13 | if self.indexes: 14 | return self._data[self.indexes.pop(0)] 15 | raise StopIteration 16 | 17 | 18 | oddeven = OddEven("0123456789") 19 | print("".join(c for c in oddeven)) # 0246813579 20 | 21 | oddeven = OddEven("ABCD") # or manually... 22 | it = iter(oddeven) # this calls oddeven.__iter__ internally 23 | print(next(it)) # A 24 | print(next(it)) # C 25 | print(next(it)) # B 26 | print(next(it)) # D 27 | 28 | 29 | # make sure it works correctly with edge cases 30 | oddeven = OddEven("") 31 | print(" ".join(c for c in oddeven)) 32 | 33 | oddeven = OddEven("A") 34 | print(" ".join(c for c in oddeven)) 35 | 36 | oddeven = OddEven("Ab") 37 | print(" ".join(c for c in oddeven)) 38 | 39 | oddeven = OddEven("AbC") 40 | print(" ".join(c for c in oddeven)) 41 | 42 | 43 | """ 44 | $ python iterators/iterator.py 45 | 0246813579 46 | A 47 | C 48 | B 49 | D 50 | 51 | A 52 | A b 53 | A C b 54 | """ 55 | -------------------------------------------------------------------------------- /ch06/oop/class.attribute.shadowing.py: -------------------------------------------------------------------------------- 1 | # oop/class.attribute.shadowing.py 2 | class Point: 3 | x = 10 4 | y = 7 5 | 6 | 7 | p = Point() 8 | print(p.x) # 10 (from class attribute) 9 | print(p.y) # 7 (from class attribute) 10 | 11 | p.x = 12 # p gets its own `x` attribute 12 | print(p.x) # 12 (now found on the instance) 13 | print(Point.x) # 10 (class attribute still the same) 14 | 15 | del p.x # we delete instance attribute 16 | print(p.x) # 10 (now search has to go again to find class attr) 17 | 18 | p.z = 3 # let's make it a 3D point 19 | print(p.z) # 3 20 | 21 | print(Point.z) 22 | # AttributeError: type object 'Point' has no attribute 'z' 23 | 24 | 25 | """ 26 | $ python class.attribute.shadowing.py 27 | 10 28 | 7 29 | 12 30 | 10 31 | 10 32 | 3 33 | Traceback (most recent call last): 34 | File "class.attribute.shadowing.py", line 21, in 35 | print(Point.z) 36 | ^^^^^^^ 37 | AttributeError: type object 'Point' has no attribute 'z' 38 | """ 39 | -------------------------------------------------------------------------------- /ch06/oop/class.init.py: -------------------------------------------------------------------------------- 1 | # oop/class.init.py 2 | class Rectangle: 3 | def __init__(self, side_a, side_b): 4 | self.side_a = side_a 5 | self.side_b = side_b 6 | 7 | def area(self): 8 | return self.side_a * self.side_b 9 | 10 | 11 | r1 = Rectangle(10, 4) 12 | print(r1.side_a, r1.side_b) # 10 4 13 | print(r1.area()) # 40 14 | 15 | r2 = Rectangle(7, 3) 16 | print(r2.area()) # 21 17 | 18 | 19 | """ 20 | $ python class.init.py 21 | 10 4 22 | 40 23 | 21 24 | """ 25 | -------------------------------------------------------------------------------- /ch06/oop/class.methods.factory.py: -------------------------------------------------------------------------------- 1 | # oop/class.methods.factory.py 2 | class Point: 3 | def __init__(self, x, y): 4 | self.x = x 5 | self.y = y 6 | 7 | @classmethod 8 | def from_tuple(cls, coords): # cls is Point 9 | return cls(*coords) 10 | 11 | @classmethod 12 | def from_point(cls, point): # cls is Point 13 | return cls(point.x, point.y) 14 | 15 | 16 | p = Point.from_tuple((3, 7)) 17 | print(p.x, p.y) # 3 7 18 | q = Point.from_point(p) 19 | print(q.x, q.y) # 3 7 20 | 21 | 22 | """ 23 | $ python class.methods.factory.py 24 | 3 7 25 | 3 7 26 | """ 27 | -------------------------------------------------------------------------------- /ch06/oop/class.methods.split.py: -------------------------------------------------------------------------------- 1 | # oop/class.methods.split.py 2 | class StringUtil: 3 | @classmethod 4 | def is_palindrome(cls, s, case_insensitive=True): 5 | s = cls._strip_string(s) 6 | # For case insensitive comparison, we lower-case s 7 | if case_insensitive: 8 | s = s.lower() 9 | return cls._is_palindrome(s) 10 | 11 | @staticmethod 12 | def _strip_string(s): 13 | return "".join(c for c in s if c.isalnum()) 14 | 15 | @staticmethod 16 | def _is_palindrome(s): 17 | for c in range(len(s) // 2): 18 | if s[c] != s[-c - 1]: 19 | return False 20 | return True 21 | 22 | @staticmethod 23 | def get_unique_words(sentence): 24 | return set(sentence.split()) 25 | 26 | 27 | print(StringUtil.is_palindrome("radar")) # True 28 | print(StringUtil.is_palindrome("not a palindrome")) # False 29 | 30 | 31 | """ 32 | $ python class.methods.split.py 33 | True 34 | False 35 | """ 36 | -------------------------------------------------------------------------------- /ch06/oop/class.namespaces.py: -------------------------------------------------------------------------------- 1 | # oop/class.namespaces.py 2 | class Person: 3 | species = "Human" 4 | 5 | 6 | print(Person.species) # Human 7 | Person.alive = True # Added dynamically! 8 | print(Person.alive) # True 9 | 10 | man = Person() 11 | print(man.species) # Human (inherited) 12 | print(man.alive) # True (inherited) 13 | 14 | Person.alive = False 15 | print(man.alive) # False (inherited) 16 | 17 | man.name = "Darth" 18 | man.surname = "Vader" 19 | print(man.name, man.surname) # Darth Vader 20 | 21 | print(Person.name) 22 | # This doesn't work. We try to access an instance attribute 23 | # from a class. Doing the opposite works, but this will give 24 | # the following error: 25 | # AttributeError: type object 'Person' has no attribute 'name' 26 | 27 | 28 | """ 29 | $ python class.namespaces.py 30 | Human 31 | True 32 | Human 33 | True 34 | False 35 | Darth Vader 36 | Traceback (most recent call last): 37 | File "class.namespaces.py", line 21, in 38 | print(Person.name) 39 | ^^^^^^^^^^^ 40 | AttributeError: type object 'Person' has no attribute 'name' 41 | """ 42 | -------------------------------------------------------------------------------- /ch06/oop/class.price.py: -------------------------------------------------------------------------------- 1 | # oop/class.price.py 2 | class Price: 3 | def final_price(self, vat, discount=0): 4 | """Returns price after applying vat and fixed discount.""" 5 | return (self.net_price * (100 + vat) / 100) - discount 6 | 7 | 8 | p1 = Price() 9 | p1.net_price = 100 10 | print(Price.final_price(p1, 20, 10)) # 110 (100 * 1.2 - 10) 11 | print(p1.final_price(20, 10)) # equivalent 12 | 13 | 14 | """ 15 | $ python class.price.py 16 | 110.0 17 | 110.0 18 | """ 19 | -------------------------------------------------------------------------------- /ch06/oop/class.self.py: -------------------------------------------------------------------------------- 1 | # oop/class.self.py 2 | class Square: 3 | side = 8 4 | 5 | def area(self): # self is a reference to an instance 6 | return self.side**2 7 | 8 | 9 | sq = Square() 10 | print(sq.area()) # 64 (side is found on the class) 11 | print(Square.area(sq)) # 64 (equivalent to sq.area()) 12 | 13 | sq.side = 10 14 | print(sq.area()) # 100 (side is found on the instance) 15 | 16 | 17 | """ 18 | $ python class.self.py 19 | 64 20 | 64 21 | 100 22 | """ 23 | -------------------------------------------------------------------------------- /ch06/oop/class_inheritance.py: -------------------------------------------------------------------------------- 1 | # oop/class_inheritance.py 2 | class Engine: 3 | def start(self): 4 | pass 5 | 6 | def stop(self): 7 | pass 8 | 9 | 10 | class ElectricEngine(Engine): # Is-A Engine 11 | pass 12 | 13 | 14 | class V8Engine(Engine): # Is-A Engine 15 | pass 16 | 17 | 18 | class Car: 19 | engine_cls = Engine 20 | 21 | def __init__(self): 22 | self.engine = self.engine_cls() # Has-A Engine 23 | 24 | def start(self): 25 | print( 26 | f"Starting {self.engine.__class__.__name__} for " 27 | f"{self.__class__.__name__}... Wroom, wroom!" 28 | ) 29 | self.engine.start() 30 | 31 | def stop(self): 32 | self.engine.stop() 33 | 34 | 35 | class RaceCar(Car): # Is-A Car 36 | engine_cls = V8Engine 37 | 38 | 39 | class CityCar(Car): # Is-A Car 40 | engine_cls = ElectricEngine 41 | 42 | 43 | class F1Car(RaceCar): # Is-A RaceCar and also Is-A Car 44 | pass # engine_cls same as parent 45 | 46 | 47 | car = Car() 48 | racecar = RaceCar() 49 | citycar = CityCar() 50 | f1car = F1Car() 51 | cars = [car, racecar, citycar, f1car] 52 | 53 | for car in cars: 54 | car.start() 55 | 56 | """ 57 | $ python class_inheritance.py 58 | Starting Engine for Car... Wroom, wroom! 59 | Starting V8Engine for RaceCar... Wroom, wroom! 60 | Starting ElectricEngine for CityCar... Wroom, wroom! 61 | Starting V8Engine for F1Car... Wroom, wroom! 62 | """ 63 | -------------------------------------------------------------------------------- /ch06/oop/dataclass.py: -------------------------------------------------------------------------------- 1 | # oop/dataclass.py 2 | from dataclasses import dataclass 3 | 4 | 5 | @dataclass 6 | class Body: 7 | """Class to represent a physical body.""" 8 | 9 | name: str 10 | mass: float = 0.0 # Kg 11 | speed: float = 1.0 # m/s 12 | 13 | def kinetic_energy(self) -> float: 14 | return (self.mass * self.speed**2) / 2 15 | 16 | 17 | body = Body("Ball", 19, 3.1415) 18 | print(body.kinetic_energy()) # 93.755711375 Joule 19 | print(body) # Body(name='Ball', mass=19, speed=3.1415) 20 | 21 | 22 | """ 23 | $ python dataclass.py 24 | 93.755711375 25 | Body(name='Ball', mass=19, speed=3.1415) 26 | """ 27 | -------------------------------------------------------------------------------- /ch06/oop/mro.py: -------------------------------------------------------------------------------- 1 | # oop/mro.py 2 | class A: 3 | label = "a" 4 | 5 | 6 | class B(A): 7 | pass # was: label = 'b' 8 | 9 | 10 | class C(A): 11 | label = "c" 12 | 13 | 14 | class D(B, C): 15 | pass 16 | 17 | 18 | d = D() 19 | print(d.label) # 'c' 20 | print(d.__class__.mro()) # notice another way to get the MRO 21 | # prints: 22 | # [, , 23 | # , , ] 24 | 25 | 26 | """ 27 | $ python oop/mro.py 28 | c 29 | [ 30 | , , 31 | , , 32 | 33 | ] 34 | """ 35 | -------------------------------------------------------------------------------- /ch06/oop/mro.simple.py: -------------------------------------------------------------------------------- 1 | # oop/mro.simple.py 2 | class A: 3 | label = "a" 4 | 5 | 6 | class B(A): 7 | label = "b" 8 | 9 | 10 | class C(A): 11 | label = "c" 12 | 13 | 14 | class D(B, C): 15 | pass 16 | 17 | 18 | d = D() 19 | print(d.label) # Hypothetically this could be either 'b' or 'c' 20 | print(d.__class__.mro()) # notice another way to get the MRO 21 | # prints: 22 | # [, , 23 | # , , ] 24 | 25 | 26 | """ 27 | $ python mro.simple.py 28 | b 29 | [ 30 | , , 31 | , , 32 | 33 | ] 34 | """ 35 | -------------------------------------------------------------------------------- /ch06/oop/operator.overloading.py: -------------------------------------------------------------------------------- 1 | # oop/operator.overloading.py 2 | class Weird: 3 | def __init__(self, s): 4 | self._s = s 5 | 6 | def __len__(self): 7 | return len(self._s) 8 | 9 | def __bool__(self): 10 | return "42" in self._s 11 | 12 | 13 | weird = Weird("Hello! I am 9 years old!") 14 | print(len(weird)) # 24 15 | print(bool(weird)) # False 16 | 17 | weird2 = Weird("Hello! I am 42 years old!") 18 | print(len(weird2)) # 25 19 | print(bool(weird2)) # True 20 | 21 | 22 | """ 23 | $ python operator.overloading.py 24 | 24 25 | False 26 | 25 27 | True 28 | """ 29 | -------------------------------------------------------------------------------- /ch06/oop/private.attrs.fixed.py: -------------------------------------------------------------------------------- 1 | # oop/private.attrs.fixed.py 2 | class A: 3 | def __init__(self, factor): 4 | self.__factor = factor 5 | 6 | def op1(self): 7 | print("Op1 with factor {}...".format(self.__factor)) 8 | 9 | 10 | class B(A): 11 | def op2(self, factor): 12 | self.__factor = factor 13 | print("Op2 with factor {}...".format(self.__factor)) 14 | 15 | 16 | obj = B(100) 17 | obj.op1() # Op1 with factor 100... 18 | obj.op2(42) # Op2 with factor 42... 19 | obj.op1() # Op1 with factor 100... <- Now it's good! 20 | 21 | print(obj.__dict__.keys()) 22 | # dict_keys(['_A__factor', '_B__factor']) 23 | 24 | 25 | """ 26 | $ python private.attrs.fixed.py 27 | Op1 with factor 100... 28 | Op2 with factor 42... 29 | Op1 with factor 100... 30 | dict_keys(['_A__factor', '_B__factor']) 31 | """ 32 | -------------------------------------------------------------------------------- /ch06/oop/private.attrs.py: -------------------------------------------------------------------------------- 1 | # oop/private.attrs.py 2 | class A: 3 | def __init__(self, factor): 4 | self._factor = factor 5 | 6 | def op1(self): 7 | print("Op1 with factor {}...".format(self._factor)) 8 | 9 | 10 | class B(A): 11 | def op2(self, factor): 12 | self._factor = factor 13 | print("Op2 with factor {}...".format(self._factor)) 14 | 15 | 16 | obj = B(100) 17 | obj.op1() # Op1 with factor 100... 18 | obj.op2(42) # Op2 with factor 42... 19 | obj.op1() # Op1 with factor 42... <- This is BAD 20 | 21 | print(obj.__dict__.keys()) 22 | # dict_keys(['_factor']) 23 | 24 | 25 | """ 26 | $ python private.attrs.py 27 | Op1 with factor 100... 28 | Op2 with factor 42... 29 | Op1 with factor 42... 30 | dict_keys(['_factor']) 31 | """ 32 | -------------------------------------------------------------------------------- /ch06/oop/property.py: -------------------------------------------------------------------------------- 1 | # oop/property.py 2 | class Person: 3 | def __init__(self, age): 4 | self.age = age # anyone can modify this freely 5 | 6 | 7 | class PersonWithAccessors: 8 | def __init__(self, age): 9 | self._age = age 10 | 11 | def get_age(self): 12 | return self._age 13 | 14 | def set_age(self, age): 15 | if 18 <= age <= 99: 16 | self._age = age 17 | else: 18 | raise ValueError("Age must be within [18, 99]") 19 | 20 | 21 | class PersonPythonic: 22 | def __init__(self, age): 23 | self._age = age 24 | 25 | @property 26 | def age(self): 27 | return self._age 28 | 29 | @age.setter 30 | def age(self, age): 31 | if 18 <= age <= 99: 32 | self._age = age 33 | else: 34 | raise ValueError("Age must be within [18, 99]") 35 | 36 | 37 | person = PersonPythonic(39) 38 | print(person.age) # 39 - Notice we access as data attribute 39 | person.age = 42 # Notice we access as data attribute 40 | print(person.age) # 42 41 | person.age = 100 # ValueError: Age must be within [18, 99] 42 | 43 | 44 | """ 45 | $ python property.py 46 | 39 47 | 42 48 | Traceback (most recent call last): 49 | File "property.py", line 41, in 50 | person.age = 100 # ValueError: Age must be within [18, 99] 51 | ^^^^^^^^^^ 52 | File "property.py", line 34, in age 53 | raise ValueError("Age must be within [18, 99]") 54 | ValueError: Age must be within [18, 99] 55 | """ 56 | -------------------------------------------------------------------------------- /ch06/oop/simplest.class.py: -------------------------------------------------------------------------------- 1 | # oop/simplest.class.py 2 | class Simplest: # when empty, the braces are optional 3 | pass 4 | 5 | 6 | print(type(Simplest)) # what type is this object? 7 | 8 | simp = Simplest() # we create an instance of Simplest: simp 9 | print(type(simp)) # what type is simp? 10 | # is simp an instance of Simplest? 11 | print(type(simp) is Simplest) # There's a better way to do this 12 | 13 | 14 | """ 15 | $ python simplest.class.py 16 | 17 | 18 | True 19 | """ 20 | -------------------------------------------------------------------------------- /ch06/oop/static.methods.py: -------------------------------------------------------------------------------- 1 | # oop/static.methods.py 2 | class StringUtil: 3 | @staticmethod 4 | def is_palindrome(s, case_insensitive=True): 5 | # we allow only letters and numbers 6 | s = "".join(c for c in s if c.isalnum()) # Study this! 7 | # For case insensitive comparison, we lower-case s 8 | if case_insensitive: 9 | s = s.lower() 10 | for c in range(len(s) // 2): 11 | if s[c] != s[-c - 1]: 12 | return False 13 | return True 14 | 15 | @staticmethod 16 | def get_unique_words(sentence): 17 | return set(sentence.split()) 18 | 19 | 20 | print( 21 | StringUtil.is_palindrome("Radar", case_insensitive=False) 22 | ) # False: Case Sensitive 23 | print(StringUtil.is_palindrome("A nut for a jar of tuna")) # True 24 | print(StringUtil.is_palindrome("Never Odd, Or Even!")) # True 25 | print( 26 | StringUtil.is_palindrome( 27 | "In Girum Imus Nocte Et Consumimur Igni" 28 | ) # Latin palindrome 29 | ) # True 30 | 31 | print( 32 | StringUtil.get_unique_words( 33 | "I love palindromes. I really really love them!" 34 | ) 35 | ) 36 | # {'them!', 'palindromes.', 'I', 'really', 'love'} 37 | 38 | 39 | """ 40 | $ python static.methods.py 41 | False 42 | True 43 | True 44 | True 45 | {'them!', 'palindromes.', 'I', 'really', 'love'} 46 | """ 47 | -------------------------------------------------------------------------------- /ch06/oop/super.duplication.py: -------------------------------------------------------------------------------- 1 | # oop/super.duplication.py 2 | class Book: 3 | def __init__(self, title, publisher, pages): 4 | self.title = title 5 | self.publisher = publisher 6 | self.pages = pages 7 | 8 | 9 | class Ebook(Book): 10 | def __init__(self, title, publisher, pages, format_): 11 | self.title = title 12 | self.publisher = publisher 13 | self.pages = pages 14 | self.format_ = format_ 15 | -------------------------------------------------------------------------------- /ch06/oop/super.explicit.py: -------------------------------------------------------------------------------- 1 | # oop/super.explicit.py 2 | class Book: 3 | def __init__(self, title, publisher, pages): 4 | self.title = title 5 | self.publisher = publisher 6 | self.pages = pages 7 | 8 | 9 | class Ebook(Book): 10 | def __init__(self, title, publisher, pages, format_): 11 | Book.__init__(self, title, publisher, pages) 12 | self.format_ = format_ 13 | 14 | 15 | ebook = Ebook( 16 | "Learn Python Programming", "Packt Publishing", 500, "PDF" 17 | ) 18 | print(ebook.title) # Learn Python Programming 19 | print(ebook.publisher) # Packt Publishing 20 | print(ebook.pages) # 500 21 | print(ebook.format_) # PDF 22 | 23 | 24 | """ 25 | $ python super.explicit.py 26 | Learn Python Programming 27 | Packt Publishing 28 | 500 29 | PDF 30 | """ 31 | -------------------------------------------------------------------------------- /ch06/oop/super.implicit.py: -------------------------------------------------------------------------------- 1 | # oop/super.implicit.py 2 | class Book: 3 | def __init__(self, title, publisher, pages): 4 | self.title = title 5 | self.publisher = publisher 6 | self.pages = pages 7 | 8 | 9 | class Ebook(Book): 10 | def __init__(self, title, publisher, pages, format_): 11 | super().__init__(title, publisher, pages) 12 | # Another way to do the same thing is: 13 | # super(Ebook, self).__init__(title, publisher, pages) 14 | self.format_ = format_ 15 | 16 | 17 | ebook = Ebook( 18 | "Learn Python Programming", "Packt Publishing", 500, "PDF" 19 | ) 20 | print(ebook.title) # Learn Python Programming 21 | print(ebook.publisher) # Packt Publishing 22 | print(ebook.pages) # 500 23 | print(ebook.format_) # PDF 24 | 25 | 26 | """ 27 | $ python super.implicit.py 28 | Learn Python Programming 29 | Packt Publishing 30 | 500 31 | PDF 32 | """ 33 | -------------------------------------------------------------------------------- /ch07/context/decimal.prec.ctx.py: -------------------------------------------------------------------------------- 1 | # context/decimal.prec.ctx.py 2 | from decimal import Context, Decimal, localcontext 3 | 4 | 5 | one = Decimal("1") 6 | three = Decimal("3") 7 | 8 | with localcontext(Context(prec=5)) as ctx: 9 | print("Custom decimal context:", one / three) 10 | 11 | print("Original context restored:", one / three) 12 | -------------------------------------------------------------------------------- /ch07/context/decimal.prec.py: -------------------------------------------------------------------------------- 1 | # context/decimal.prec.py 2 | from decimal import Context, Decimal, getcontext, setcontext 3 | 4 | one = Decimal("1") 5 | three = Decimal("3") 6 | 7 | orig_ctx = getcontext() 8 | ctx = Context(prec=5) 9 | setcontext(ctx) 10 | print(f"{ctx}\n") 11 | 12 | print("Custom decimal context:", one / three) 13 | setcontext(orig_ctx) 14 | print("Original context restored:", one / three) 15 | 16 | 17 | """ 18 | Context(prec=5, rounding=ROUND_HALF_EVEN, Emin=-999999, 19 | Emax=999999, capitals=1, clamp=0, flags=[], 20 | traps=[InvalidOperation, DivisionByZero, Overflow]) 21 | 22 | Custom decimal context: 0.33333 23 | Original context restored: 0.3333333333333333333333333333 24 | """ 25 | -------------------------------------------------------------------------------- /ch07/context/decimal.prec.try.py: -------------------------------------------------------------------------------- 1 | # context/decimal.prec.try.py 2 | from decimal import Context, Decimal, getcontext, setcontext 3 | 4 | one = Decimal("1") 5 | three = Decimal("3") 6 | 7 | orig_ctx = getcontext() 8 | ctx = Context(prec=5) 9 | setcontext(ctx) 10 | try: 11 | print("Custom decimal context:", one / three) 12 | finally: 13 | setcontext(orig_ctx) 14 | print("Original context restored:", one / three) 15 | -------------------------------------------------------------------------------- /ch07/context/generator.py: -------------------------------------------------------------------------------- 1 | # context/generator.py 2 | from contextlib import contextmanager 3 | 4 | 5 | @contextmanager 6 | def my_context_manager(): 7 | print("Entering 'with' context") 8 | val = object() 9 | print(id(val)) 10 | try: 11 | yield val 12 | except Exception as e: 13 | print(f"{type(e)=} {e=} {e.__traceback__=}") 14 | finally: 15 | print("Exiting 'with' context") 16 | 17 | 18 | print("About to enter 'with' context") 19 | with my_context_manager() as val: 20 | print("Inside 'with' context") 21 | print(id(val)) 22 | raise Exception("Exception inside 'with' context") 23 | print("This line will never be reached") 24 | 25 | print("After 'with' context") 26 | 27 | 28 | """ 29 | $ python context/generator.py 30 | About to enter 'with' context 31 | Entering 'with' context 32 | 139768531985040 33 | Inside 'with' context 34 | 139768531985040 35 | type(e)= e=Exception("Exception inside 'with' 36 | context") e.__traceback__= 37 | Exiting 'with' context 38 | After 'with' context 39 | """ 40 | -------------------------------------------------------------------------------- /ch07/context/manager.class.py: -------------------------------------------------------------------------------- 1 | # context/manager.class.py 2 | class MyContextManager: 3 | def __init__(self): 4 | print("MyContextManager init", id(self)) 5 | 6 | def __enter__(self): 7 | print("Entering 'with' context") 8 | return self 9 | 10 | def __exit__(self, exc_type, exc_val, exc_tb): 11 | print(f"{exc_type=} {exc_val=} {exc_tb=}") 12 | print("Exiting 'with' context") 13 | return True 14 | 15 | 16 | ctx_mgr = MyContextManager() 17 | 18 | print("About to enter 'with' context") 19 | 20 | with ctx_mgr as mgr: 21 | print("Inside 'with' context") 22 | print(id(mgr)) 23 | raise Exception("Exception inside 'with' context") 24 | print("This line will never be reached") 25 | 26 | print("After 'with' context") 27 | 28 | 29 | """ 30 | $ python context/manager.class.py 31 | MyContextManager init 140340228792272 32 | About to enter 'with' context 33 | Entering 'with' context 34 | Inside 'with' context 35 | 140340228792272 36 | exc_type= exc_val=Exception("Exception inside 37 | 'with' context") exc_tb= 38 | Exiting 'with' context 39 | After 'with' context 40 | """ 41 | -------------------------------------------------------------------------------- /ch07/context/multiple.py: -------------------------------------------------------------------------------- 1 | # context/multiple.py 2 | from decimal import Context, Decimal, localcontext 3 | 4 | 5 | one = Decimal("1") 6 | three = Decimal("3") 7 | 8 | with ( 9 | localcontext(Context(prec=5)), 10 | open("output.txt", "w") as out_file, 11 | ): 12 | out_file.write(f"{one} / {three} = {one / three}\n") 13 | 14 | # Before Python 3.10 the above code would have to be written as: 15 | # with localcontext(Context(prec=5)), \ 16 | # open("output.txt", "w") as out_file: 17 | # out_file.write(f"{one} / {three} = {one / three}\n") 18 | -------------------------------------------------------------------------------- /ch07/exceptions/first.example.txt: -------------------------------------------------------------------------------- 1 | # exceptions/first.example.txt 2 | # a few examples of exceptions 3 | 4 | >>> gen = (n for n in range(2)) 5 | >>> next(gen) 6 | 0 7 | >>> next(gen) 8 | 1 9 | >>> next(gen) 10 | Traceback (most recent call last): 11 | File "", line 1, in 12 | StopIteration 13 | >>> print(undefined_name) 14 | Traceback (most recent call last): 15 | File "", line 1, in 16 | NameError: name 'undefined_name' is not defined 17 | >>> mylist = [1, 2, 3] 18 | >>> mylist[5] 19 | Traceback (most recent call last): 20 | File "", line 1, in 21 | IndexError: list index out of range 22 | >>> mydict = {"a": "A", "b": "B"} 23 | >>> mydict["c"] 24 | Traceback (most recent call last): 25 | File "", line 1, in 26 | KeyError: 'c' 27 | >>> 1 / 0 28 | Traceback (most recent call last): 29 | File "", line 1, in 30 | ZeroDivisionError: division by zero 31 | -------------------------------------------------------------------------------- /ch07/exceptions/for.loop.py: -------------------------------------------------------------------------------- 1 | # exceptions/for.loop.py 2 | n = 100 3 | found = False 4 | for a in range(n): 5 | if found: 6 | break 7 | for b in range(n): 8 | if found: 9 | break 10 | for c in range(n): 11 | if 42 * a + 17 * b + c == 5096: 12 | found = True 13 | print(a, b, c) # 79 99 95 14 | break 15 | 16 | 17 | class ExitLoopException(Exception): 18 | pass 19 | 20 | 21 | try: 22 | n = 100 23 | for a in range(n): 24 | for b in range(n): 25 | for c in range(n): 26 | if 42 * a + 17 * b + c == 5096: 27 | raise ExitLoopException(a, b, c) 28 | except ExitLoopException as ele: 29 | print(ele.args) # (79, 99, 95) 30 | -------------------------------------------------------------------------------- /ch07/exceptions/groups/exc.group.txt: -------------------------------------------------------------------------------- 1 | # exceptions/groups/exc.group.txt 2 | >>> from util import validate_ages 3 | >>> validate_ages([24, -5, "ninety", 30, None]) 4 | + Exception Group Traceback (most recent call last): 5 | | File "", line 1, in 6 | | File "exceptions/groups/util.py", line 20, in validate_ages 7 | | raise ExceptionGroup("Validation errors", errors) 8 | | ExceptionGroup: Validation errors (3 sub-exceptions) 9 | +-+---------------- 1 ---------------- 10 | | Traceback (most recent call last): 11 | | File "exceptions/groups/util.py", line 15, in validate_ages 12 | | validate_age(age) 13 | | File "exceptions/groups/util.py", line 8, in validate_age 14 | | raise ValueError(f"Negative age: {age}") 15 | | ValueError: Negative age: -5 16 | +---------------- 2 ---------------- 17 | | Traceback (most recent call last): 18 | | File "exceptions/groups/util.py", line 15, in validate_ages 19 | | validate_age(age) 20 | | File "exceptions/groups/util.py", line 6, in validate_age 21 | | raise TypeError(f"Not an integer: {age}") 22 | | TypeError: Not an integer: ninety 23 | +---------------- 3 ---------------- 24 | | Traceback (most recent call last): 25 | | File "exceptions/groups/util.py", line 15, in validate_ages 26 | | validate_age(age) 27 | | File "exceptions/groups/util.py", line 6, in validate_age 28 | | raise TypeError(f"Not an integer: {age}") 29 | | TypeError: Not an integer: None 30 | +------------------------------------ 31 | -------------------------------------------------------------------------------- /ch07/exceptions/groups/handle.group.txt: -------------------------------------------------------------------------------- 1 | # exceptions/groups/handle.group.txt 2 | >>> from util import validate_ages 3 | >>> try: 4 | ... validate_ages([24, -5, "ninety", 30, None]) 5 | ... except ExceptionGroup as e: 6 | ... print(e) 7 | ... print(e.exceptions) 8 | ... 9 | Validation errors (3 sub-exceptions) 10 | (ValueError('Negative age', -5), 11 | TypeError('Not an integer', 'ninety'), 12 | TypeError('Not an integer', None)) 13 | -------------------------------------------------------------------------------- /ch07/exceptions/groups/util.py: -------------------------------------------------------------------------------- 1 | # exceptions/groups/util.py 2 | 3 | 4 | def validate_age(age): 5 | if not isinstance(age, int): 6 | raise TypeError(f"Not an integer: {age}") 7 | if age < 0: 8 | raise ValueError(f"Negative age: {age}") 9 | 10 | 11 | def validate_ages(ages): 12 | errors = [] 13 | for age in ages: 14 | try: 15 | validate_age(age) 16 | except Exception as e: 17 | errors.append(e) 18 | 19 | if errors: 20 | raise ExceptionGroup("Validation errors", errors) 21 | -------------------------------------------------------------------------------- /ch07/exceptions/multiple.py: -------------------------------------------------------------------------------- 1 | # exceptions/multiple.py 2 | values = (1, 2) # try (1, 0) and ('one', 2) 3 | 4 | try: 5 | q, r = divmod(*values) 6 | except (ZeroDivisionError, TypeError) as e: 7 | print(type(e), e) 8 | 9 | 10 | try: 11 | q, r = divmod(*values) 12 | except ZeroDivisionError: 13 | print("You tried to divide by zero!") 14 | except TypeError as e: 15 | print(e) 16 | -------------------------------------------------------------------------------- /ch07/exceptions/note.py: -------------------------------------------------------------------------------- 1 | # exceptions/note.py 2 | def squareroot(number): 3 | if number < 0: 4 | raise ValueError("No negative numbers please") 5 | return number**0.5 6 | 7 | 8 | def quadratic(a, b, c): 9 | d = b**2 - 4 * a * c 10 | try: 11 | return ( 12 | (-b - squareroot(d)) / (2 * a), 13 | (-b + squareroot(d)) / (2 * a), 14 | ) 15 | except ValueError as e: 16 | e.add_note(f"Cannot solve {a}x**2 + {b}x + {c} == 0") 17 | raise 18 | 19 | 20 | quadratic(1, 0, 1) 21 | -------------------------------------------------------------------------------- /ch07/exceptions/raising.txt: -------------------------------------------------------------------------------- 1 | # exceptions/raising.txt 2 | >>> raise NotImplementedError("I'm afraid I can't do that") 3 | Traceback (most recent call last): 4 | File "", line 1, in 5 | NotImplementedError: I'm afraid I can't do that 6 | -------------------------------------------------------------------------------- /ch07/exceptions/replace.txt: -------------------------------------------------------------------------------- 1 | # exceptions/replace.txt 2 | >>> class NotFoundError(Exception): 3 | ... pass 4 | ... 5 | >>> vowels = {"a": 1, "e": 5, "i": 9, "o": 15, "u": 21} 6 | >>> try: 7 | ... pos = vowels["y"] 8 | ... except KeyError as e: 9 | ... raise NotFoundError(*e.args) 10 | ... 11 | Traceback (most recent call last): 12 | File "", line 2, in 13 | KeyError: 'y' 14 | 15 | During handling of the above exception, another exception occurred: 16 | 17 | Traceback (most recent call last): 18 | File "", line 4, in 19 | NotFoundError: y 20 | >>> try: 21 | ... pos = vowels["y"] 22 | ... except KeyError as e: 23 | ... raise NotFoundError(*e.args) from e 24 | ... 25 | Traceback (most recent call last): 26 | File "", line 2, in 27 | KeyError: 'y' 28 | 29 | The above exception was the direct cause of the following exception: 30 | 31 | Traceback (most recent call last): 32 | File "", line 4, in 33 | NotFoundError: y 34 | -------------------------------------------------------------------------------- /ch07/exceptions/trace.back.py: -------------------------------------------------------------------------------- 1 | # exceptions/trace.back.py 2 | def squareroot(number): 3 | if number < 0: 4 | raise ValueError("No negative numbers please") 5 | return number**0.5 6 | 7 | 8 | def quadratic(a, b, c): 9 | d = b**2 - 4 * a * c 10 | return ( 11 | (-b - squareroot(d)) / (2 * a), 12 | (-b + squareroot(d)) / (2 * a), 13 | ) 14 | 15 | 16 | quadratic(1, 0, 1) # x**2 + 1 == 0 17 | 18 | 19 | """ 20 | $ python exceptions/trace.back.py 21 | Traceback (most recent call last): 22 | File "exceptions/trace.back.py", line 12, in 23 | quadratic(1, 0, 1) # x**2 + 1 == 0 24 | File "exceptions/trace.back.py", line 9, in quadratic 25 | return ((-b - squareroot(d)) / (2 * a), 26 | File "exceptions/trace.back.py", line 4, in squareroot 27 | raise ValueError("No negative numbers please") 28 | ValueError: No negative numbers please 29 | """ 30 | -------------------------------------------------------------------------------- /ch07/exceptions/try.syntax.py: -------------------------------------------------------------------------------- 1 | # exceptions/try.syntax.py 2 | 3 | 4 | def try_syntax(numerator, denominator): 5 | try: 6 | print(f"In the try block: {numerator}/{denominator}") 7 | result = numerator / denominator 8 | except ZeroDivisionError as zde: 9 | print(zde) 10 | else: 11 | print("The result is:", result) 12 | return result 13 | finally: 14 | print("Exiting") 15 | 16 | 17 | print(try_syntax(12, 4)) 18 | print(try_syntax(11, 0)) 19 | 20 | 21 | """ 22 | $ python try.syntax.py 23 | In the try block: 12/4 # try 24 | The result is: 3.0 # else 25 | Exiting # finally 26 | 3.0 # return within else 27 | 28 | In the try block: 11/0 # try 29 | division by zero # except 30 | Exiting # finally 31 | None # implicit return end of function 32 | """ 33 | -------------------------------------------------------------------------------- /ch07/exceptions/unhandled.py: -------------------------------------------------------------------------------- 1 | # exceptions/unhandled.py 2 | 1 + "one" 3 | print("This line will never be reached") 4 | 5 | """ 6 | $ python exceptions/unhandled.py 7 | Traceback (most recent call last): 8 | File "exceptions/unhandled.py", line 3, in 9 | 1 + "one" 10 | TypeError: unsupported operand type(s) for +: 'int' and 'str' 11 | """ 12 | -------------------------------------------------------------------------------- /ch08/config_files/book-version/config-ini.txt: -------------------------------------------------------------------------------- 1 | >>> import configparser 2 | >>> config = configparser.ConfigParser() 3 | >>> config.read("config.ini") 4 | ['config.ini'] 5 | >>> config.sections() 6 | ['owner', 'database', 'database.primary', 'database.secondary'] 7 | >>> config.items("database") 8 | [ 9 | ('title', 'Config INI example'), ('host', '192.168.1.255'), 10 | ('user', 'redis'), ('password', 'redis-password'), 11 | ('db_range', '[0, 32]') 12 | ] 13 | >>> config["database"] 14 | 15 | >>> dict(config["database"]) 16 | { 17 | 'host': '192.168.1.255', 'user': 'redis', 18 | 'password': 'redis-password', 'db_range': '[0, 32]', 19 | 'title': 'Config INI example' 20 | } 21 | >>> config["DEFAULT"]["host"] 22 | '192.168.1.1' 23 | >>> dict(config["database.secondary"]) 24 | { 25 | 'port': '6380', 'connection_max': '4000', 26 | 'title': 'Config INI example', 'host': '192.168.1.1' 27 | } 28 | >>> config.getint("database.primary", "port") 29 | 6379 30 | -------------------------------------------------------------------------------- /ch08/config_files/book-version/config-toml.txt: -------------------------------------------------------------------------------- 1 | >>> import tomllib 2 | >>> with open("config.toml", "rb") as f: 3 | ... config = tomllib.load(f) 4 | ... 5 | >>> config 6 | { 7 | 'title': 'Config Example', 8 | 'owner': { 9 | 'name': 'Fabrizio Romano', 10 | 'dob': datetime.datetime( 11 | 1975, 12, 29, 11, 50, tzinfo=datetime.timezone.utc 12 | ) 13 | }, 14 | 'database': { 15 | 'host': '192.168.1.255', 16 | 'user': 'redis', 17 | 'password': 'redis-password', 18 | 'db_range': [0, 32], 19 | 'primary': {'port': 6379, 'connection_max': 5000}, 20 | 'secondary': {'port': 6380, 'connection_max': 4000} 21 | } 22 | } 23 | >>> config["title"] 24 | 'Config Example' 25 | >>> config["owner"] 26 | { 27 | 'name': 'Fabrizio Romano', 28 | 'dob': datetime.datetime( 29 | 1975, 12, 29, 11, 50, tzinfo=datetime.timezone.utc 30 | ) 31 | } 32 | >>> config["database"]["primary"] 33 | {'port': 6379, 'connection_max': 5000} 34 | >>> config["database"]["db_range"] 35 | [0, 32] 36 | -------------------------------------------------------------------------------- /ch08/config_files/config-ini.txt: -------------------------------------------------------------------------------- 1 | # Test with: 2 | # $ python -m doctest -v config-ini.txt 3 | 4 | >>> import configparser 5 | >>> config = configparser.ConfigParser() 6 | >>> config.read("config.ini") 7 | ['config.ini'] 8 | >>> config.sections() 9 | ['owner', 'database', 'database.primary', 'database.secondary'] 10 | >>> config.items("database") 11 | [('title', 'Config INI example'), ('host', '192.168.1.255'), ('user', 'redis'), ('password', 'redis-password'), ('db_range', '[0, 32]')] 12 | >>> config["database"] 13 | 14 | >>> dict(config["database"]) 15 | {'host': '192.168.1.255', 'user': 'redis', 'password': 'redis-password', 'db_range': '[0, 32]', 'title': 'Config INI example'} 16 | >>> config["DEFAULT"]["host"] 17 | '192.168.1.1' 18 | >>> dict(config["database.secondary"]) 19 | {'port': '6380', 'connection_max': '4000', 'title': 'Config INI example', 'host': '192.168.1.1'} 20 | >>> config.getint("database.primary", "port") 21 | 6379 22 | -------------------------------------------------------------------------------- /ch08/config_files/config-toml.txt: -------------------------------------------------------------------------------- 1 | # Test with: 2 | # $ python -m doctest -v config-toml.txt 3 | 4 | >>> import tomllib 5 | >>> with open("config.toml", "rb") as f: 6 | ... config = tomllib.load(f) 7 | ... 8 | >>> config 9 | {'title': 'Config Example', 'owner': {'name': 'Fabrizio Romano', 'dob': datetime.datetime(1975, 12, 29, 11, 50, tzinfo=datetime.timezone.utc)}, 'database': {'host': '192.168.1.255', 'user': 'redis', 'password': 'redis-password', 'db_range': [0, 32], 'primary': {'port': 6379, 'connection_max': 5000}, 'secondary': {'port': 6380, 'connection_max': 4000}}} 10 | >>> config["title"] 11 | 'Config Example' 12 | >>> config["owner"] 13 | {'name': 'Fabrizio Romano', 'dob': datetime.datetime(1975, 12, 29, 11, 50, tzinfo=datetime.timezone.utc)} 14 | >>> config["database"]["primary"] 15 | {'port': 6379, 'connection_max': 5000} 16 | >>> config["database"]["db_range"] 17 | [0, 32] 18 | -------------------------------------------------------------------------------- /ch08/config_files/config.ini: -------------------------------------------------------------------------------- 1 | [owner] 2 | name = Fabrizio Romano 3 | dob = 1975-12-29T11:50:00Z 4 | 5 | [DEFAULT] 6 | title = Config INI example 7 | host = 192.168.1.1 8 | 9 | [database] 10 | host = 192.168.1.255 11 | user = redis 12 | password = redis-password 13 | db_range = [0, 32] 14 | 15 | [database.primary] 16 | port = 6379 17 | connection_max = 5000 18 | 19 | [database.secondary] 20 | port = 6380 21 | connection_max = 4000 22 | 23 | -------------------------------------------------------------------------------- /ch08/config_files/config.toml: -------------------------------------------------------------------------------- 1 | title = "Config Example" 2 | 3 | [owner] 4 | name = "Fabrizio Romano" 5 | dob = 1975-12-29T11:50:00Z 6 | 7 | [database] 8 | host = "192.168.1.255" 9 | user = "redis" 10 | password = "redis-password" 11 | db_range = [0, 32] 12 | 13 | [database.primary] 14 | port = 6379 15 | connection_max = 5000 16 | 17 | [database.secondary] 18 | port = 6380 19 | connection_max = 4000 20 | -------------------------------------------------------------------------------- /ch08/files/compression/content1.txt: -------------------------------------------------------------------------------- 1 | This is content1.txt -------------------------------------------------------------------------------- /ch08/files/compression/content2.txt: -------------------------------------------------------------------------------- 1 | This is content2.txt -------------------------------------------------------------------------------- /ch08/files/compression/subfolder/content3.txt: -------------------------------------------------------------------------------- 1 | This is subfolder/content3.txt -------------------------------------------------------------------------------- /ch08/files/compression/subfolder/content4.txt: -------------------------------------------------------------------------------- 1 | This is subfolder/content4.txt -------------------------------------------------------------------------------- /ch08/files/compression/tar.py: -------------------------------------------------------------------------------- 1 | # files/compression/tar.py 2 | import tarfile 3 | 4 | with tarfile.open("example.tar.gz", "w:gz") as tar: 5 | tar.add("content1.txt") 6 | tar.add("content2.txt") 7 | tar.add("subfolder/content3.txt") 8 | tar.add("subfolder/content4.txt") 9 | 10 | with tarfile.open("example.tar.gz", "r:gz") as tar: 11 | tar.extractall("extract_tar", filter="data") 12 | 13 | 14 | """ 15 | SECURITY NOTE: 16 | 17 | The code in this module is for illustration purposes only. 18 | For the sake of simplicity, the code does not validate the file paths. 19 | It is not secure to extract files from an archive without validating the file paths. 20 | 21 | Please see: 22 | 23 | - https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall 24 | - https://github.com/advisories/GHSA-gw9q-c7gh-j9vm 25 | 26 | for more information. 27 | """ 28 | -------------------------------------------------------------------------------- /ch08/files/compression/zip.py: -------------------------------------------------------------------------------- 1 | # files/compression/zip.py 2 | from zipfile import ZipFile 3 | 4 | 5 | with ZipFile("example.zip", "w") as zp: 6 | zp.write("content1.txt") 7 | zp.write("content2.txt") 8 | zp.write("subfolder/content3.txt") 9 | zp.write("subfolder/content4.txt") 10 | 11 | 12 | with ZipFile("example.zip") as zp: 13 | zp.extract("content1.txt", "extract_zip") 14 | zp.extract("subfolder/content3.txt", "extract_zip") 15 | -------------------------------------------------------------------------------- /ch08/files/existence.py: -------------------------------------------------------------------------------- 1 | # files/existence.py 2 | from pathlib import Path 3 | 4 | p = Path("fear.txt") 5 | path = p.parent.absolute() 6 | 7 | print(p.is_file()) # True 8 | print(path) # /Users/fab/code/lpp4ed/ch08/files 9 | print(path.is_dir()) # True 10 | 11 | q = Path("/Users/fab/code/lpp4ed/ch08/files") 12 | print(q.is_dir()) # True 13 | -------------------------------------------------------------------------------- /ch08/files/fear.txt: -------------------------------------------------------------------------------- 1 | An excerpt from Fear - By Thich Nhat Hanh 2 | 3 | The Present Is Free from Fear 4 | 5 | When we are not fully present, we are not really living. We’re not really there, either for our loved ones or for ourselves. If we’re not there, then where are we? We are running, running, running, even during our sleep. We run because we’re trying to escape from our fear. 6 | 7 | We cannot enjoy life if we spend our time and energy worrying about what happened yesterday and what will happen tomorrow. If we’re afraid all the time, we miss out on the wonderful fact that we’re alive and can be happy right now. In everyday life, we tend to believe that happiness is only possible in the future. We’re always looking for the “right” conditions that we don’t yet have to make us happy. We ignore what is happening right in front of us. We look for something that will make us feel more solid, more safe, more secure. But we’re afraid all the time of what the future will bring—afraid we’ll lose our jobs, our possessions, the people around us whom we love. So we wait and hope for that magical moment—always sometime in the future—when everything will be as we want it to be. We forget that life is available only in the present moment. The Buddha said, “It is possible to live happily in the present moment. It is the only moment we have.” 8 | -------------------------------------------------------------------------------- /ch08/files/listing.py: -------------------------------------------------------------------------------- 1 | # files/listing.py 2 | from pathlib import Path 3 | 4 | 5 | p = Path(".") 6 | 7 | for entry in p.glob("*"): 8 | print("File:" if entry.is_file() else "Folder:", entry) 9 | 10 | 11 | """ 12 | $ python listing.py 13 | File: existence.py 14 | File: manipulation.py 15 | File: print_file.py 16 | File: read_write_bin.py 17 | File: paths.py 18 | File: open_with.py 19 | File: walking.py 20 | File: tmp.py 21 | File: read_write.py 22 | File: listing.py 23 | File: write_not_exists.py 24 | Folder: compression 25 | File: ops_create.py 26 | File: fear.txt 27 | File: open_try.py 28 | File: walking.pathlib.py 29 | """ 30 | -------------------------------------------------------------------------------- /ch08/files/manipulation.py: -------------------------------------------------------------------------------- 1 | # files/manipulation.py 2 | from collections import Counter 3 | from string import ascii_letters 4 | 5 | 6 | chars = ascii_letters + " " 7 | 8 | 9 | def sanitize(s, chars): 10 | return "".join(c for c in s if c in chars) 11 | 12 | 13 | def reverse(s): 14 | return s[::-1] 15 | 16 | 17 | with open("fear.txt") as stream: 18 | lines = [line.rstrip() for line in stream] 19 | 20 | 21 | # let us write the mirrored version of the file 22 | with open("raef.txt", "w") as stream: 23 | stream.write("\n".join(reverse(line) for line in lines)) 24 | 25 | 26 | # now we can calculate some statistics 27 | lines = [sanitize(line, chars) for line in lines] 28 | whole = " ".join(lines) 29 | 30 | 31 | # we perform comparisons on the lowercased version of `whole` 32 | cnt = Counter(whole.lower().split()) 33 | 34 | # we can print the N most common words 35 | print(cnt.most_common(3)) # [('we', 17), ('the', 13), ('were', 7)] 36 | -------------------------------------------------------------------------------- /ch08/files/open_try.py: -------------------------------------------------------------------------------- 1 | # files/open_try.py 2 | fh = open("fear.txt", "rt") # r: read, t: text 3 | 4 | for line in fh.readlines(): 5 | print(line.strip()) # remove whitespace and print 6 | 7 | fh.close() 8 | 9 | 10 | # secured by try/finally 11 | fh = open("fear.txt", "rt") 12 | 13 | try: 14 | for line in fh.readlines(): 15 | print(line.strip()) 16 | 17 | finally: 18 | fh.close() 19 | 20 | 21 | # equivalent to: 22 | fh = open("fear.txt") # rt is default 23 | 24 | try: 25 | for line in fh: # we can iterate directly on fh 26 | print(line.strip()) 27 | 28 | finally: 29 | fh.close() 30 | -------------------------------------------------------------------------------- /ch08/files/open_with.py: -------------------------------------------------------------------------------- 1 | # files/open_with.py 2 | with open("fear.txt") as fh: 3 | for line in fh: 4 | print(line.strip()) 5 | -------------------------------------------------------------------------------- /ch08/files/ops_create.py: -------------------------------------------------------------------------------- 1 | # files/ops_create.py 2 | import shutil 3 | from pathlib import Path 4 | 5 | 6 | base_path = Path("ops_example") 7 | 8 | # let us perform an initial cleanup just in case 9 | if base_path.exists() and base_path.is_dir(): 10 | shutil.rmtree(base_path) 11 | 12 | # now we create the directory 13 | base_path.mkdir() 14 | 15 | path_b = base_path / "A" / "B" 16 | path_c = base_path / "A" / "C" 17 | path_d = base_path / "A" / "D" 18 | 19 | path_b.mkdir(parents=True) 20 | path_c.mkdir() # no need for parents now, as 'A' has been created 21 | 22 | # we add three files in `ops_example/A/B` 23 | for filename in ("ex1.txt", "ex2.txt", "ex3.txt"): 24 | with open(path_b / filename, "w") as stream: 25 | stream.write(f"Some content here in {filename}\n") 26 | 27 | 28 | shutil.move(path_b, path_d) 29 | 30 | 31 | # we can also rename files 32 | ex1 = path_d / "ex1.txt" 33 | ex1.rename(ex1.parent / "ex1.renamed.txt") 34 | 35 | # now call $ tree ops_example 36 | 37 | """ 38 | $ tree ops_example 39 | ops_example 40 | └── A 41 | ├── C 42 | └── D 43 | ├── ex1.renamed.txt 44 | ├── ex2.txt 45 | └── ex3.txt 46 | """ 47 | -------------------------------------------------------------------------------- /ch08/files/paths.py: -------------------------------------------------------------------------------- 1 | # files/paths.py 2 | from pathlib import Path 3 | 4 | 5 | p = Path("fear.txt") 6 | 7 | print(p.absolute()) 8 | print(p.name) 9 | print(p.parent.absolute()) 10 | print(p.suffix) 11 | 12 | print(p.parts) 13 | print(p.absolute().parts) 14 | 15 | readme_path = p.parent / ".." / ".." / "README.rst" 16 | print(readme_path.absolute()) 17 | print(readme_path.resolve()) 18 | 19 | 20 | """ 21 | $ python paths.py 22 | /Users/fab/code/lpp4ed/ch08/files/fear.txt 23 | fear.txt 24 | /Users/fab/code/lpp4ed/ch08/files 25 | .txt 26 | ('fear.txt',) 27 | ( 28 | '/', 'Users', 'fab', 'code', 'lpp4ed', 29 | 'ch08', 'files', 'fear.txt' 30 | ) 31 | /Users/fab/code/lpp4ed/ch08/files/../../README.rst 32 | /Users/fab/code/lpp4ed/README.rst 33 | """ 34 | -------------------------------------------------------------------------------- /ch08/files/print_file.py: -------------------------------------------------------------------------------- 1 | # files/print_file.py 2 | with open("print_example.txt", "w") as fw: 3 | print("Hey I am printing into a file!!!", file=fw) 4 | -------------------------------------------------------------------------------- /ch08/files/read_write.py: -------------------------------------------------------------------------------- 1 | # files/read_write.py 2 | with open("fear.txt") as f: 3 | lines = [line.rstrip() for line in f] 4 | 5 | 6 | with open("fear_copy.txt", "w") as fw: # w - write 7 | fw.write("\n".join(lines)) 8 | -------------------------------------------------------------------------------- /ch08/files/read_write_bin.py: -------------------------------------------------------------------------------- 1 | # files/read_write_bin.py 2 | with open("example.bin", "wb") as fw: 3 | fw.write(b"This is binary data...") 4 | 5 | 6 | with open("example.bin", "rb") as f: 7 | print(f.read()) # prints: b'This is binary data...' 8 | -------------------------------------------------------------------------------- /ch08/files/tmp.py: -------------------------------------------------------------------------------- 1 | # files/tmp.py 2 | from tempfile import NamedTemporaryFile, TemporaryDirectory 3 | 4 | 5 | with TemporaryDirectory(dir=".") as td: 6 | print("Temp directory:", td) 7 | with NamedTemporaryFile(dir=td) as t: 8 | name = t.name 9 | print(name) 10 | 11 | 12 | """ 13 | Note, your results will be different: 14 | 15 | $ python tmp.py 16 | Temp directory: /Users/fab/code/lpp4ed/ch08/files/tmpqq4quhbc 17 | /Users/fab/code/lpp4ed/ch08/files/tmpqq4quhbc/tmpypwwhpwq 18 | """ 19 | -------------------------------------------------------------------------------- /ch08/files/walking.pathlib.py: -------------------------------------------------------------------------------- 1 | # files/walking.pathlib.py 2 | from pathlib import Path 3 | 4 | 5 | p = Path(".") 6 | 7 | for root, dirs, files in p.walk(): 8 | print(f"{root=}") 9 | 10 | if dirs: 11 | print("Directories:") 12 | for dir_ in dirs: 13 | print(dir_) 14 | print() 15 | 16 | if files: 17 | print("Files:") 18 | for filename in files: 19 | print(filename) 20 | print() 21 | 22 | 23 | """ 24 | $ python walking.pathlib.py 25 | root=PosixPath('.') 26 | Directories: 27 | compression 28 | 29 | Files: 30 | existence.py 31 | manipulation.py 32 | print_file.py 33 | read_write_bin.py 34 | paths.py 35 | open_with.py 36 | walking.py 37 | tmp.py 38 | read_write.py 39 | listing.py 40 | write_not_exists.py 41 | ops_create.py 42 | fear.txt 43 | open_try.py 44 | walking.pathlib.py 45 | 46 | root=PosixPath('compression') 47 | Directories: 48 | subfolder 49 | 50 | Files: 51 | tar.py 52 | content1.txt 53 | content2.txt 54 | zip.py 55 | 56 | root=PosixPath('compression/subfolder') 57 | Files: 58 | content3.txt 59 | content4.txt 60 | """ 61 | -------------------------------------------------------------------------------- /ch08/files/walking.py: -------------------------------------------------------------------------------- 1 | # files/walking.py 2 | import os 3 | 4 | 5 | for root, dirs, files in os.walk("."): 6 | abs_root = os.path.abspath(root) 7 | print(abs_root) 8 | 9 | if dirs: 10 | print("Directories:") 11 | for dir_ in dirs: 12 | print(dir_) 13 | print() 14 | 15 | if files: 16 | print("Files:") 17 | for filename in files: 18 | print(filename) 19 | print() 20 | 21 | 22 | """ 23 | $ python walking.py 24 | /Users/fab/code/lpp4ed/ch08/files 25 | Directories: 26 | compression 27 | 28 | Files: 29 | existence.py 30 | manipulation.py 31 | print_file.py 32 | read_write_bin.py 33 | paths.py 34 | open_with.py 35 | walking.py 36 | tmp.py 37 | read_write.py 38 | listing.py 39 | write_not_exists.py 40 | ops_create.py 41 | fear.txt 42 | open_try.py 43 | walking.pathlib.py 44 | 45 | /Users/fab/code/lpp4ed/ch08/files/compression 46 | Directories: 47 | subfolder 48 | 49 | Files: 50 | tar.py 51 | content1.txt 52 | content2.txt 53 | zip.py 54 | 55 | /Users/fab/code/lpp4ed/ch08/files/compression/subfolder 56 | Files: 57 | content3.txt 58 | content4.txt 59 | """ 60 | -------------------------------------------------------------------------------- /ch08/files/write_not_exists.py: -------------------------------------------------------------------------------- 1 | # files/write_not_exists.py 2 | with open("write_x.txt", "x") as fw: # this succeeds 3 | fw.write("Writing line 1") 4 | 5 | 6 | with open("write_x.txt", "x") as fw: # this fails 7 | fw.write("Writing line 2") 8 | 9 | 10 | """ 11 | $ python write_not_exists.py 12 | Traceback (most recent call last): 13 | File "write_not_exists.py", line 6, in 14 | with open("write_x.txt", "x") as fw: # this fails 15 | ^^^^^^^^^^^^^^^^^^^^^^^^ 16 | FileExistsError: [Errno 17] File exists: 'write_x.txt' 17 | """ 18 | -------------------------------------------------------------------------------- /ch08/io_examples/reqs_post.py: -------------------------------------------------------------------------------- 1 | # io_examples/reqs_post.py 2 | import requests 3 | 4 | 5 | url = "https://httpbin.org/post" 6 | data = dict(title="Learn Python Programming") 7 | 8 | 9 | resp = requests.post(url, data=data) 10 | print("Response for POST") 11 | print(resp.json()) 12 | 13 | 14 | """ 15 | $ python reqs_post.py 16 | 17 | Response for POST 18 | { 19 | "args": {}, 20 | "data": "", 21 | "files": {}, 22 | "form": {"title": "Learn Python Programming"}, 23 | "headers": { 24 | "Accept": "*/*", 25 | "Accept-Encoding": "gzip, deflate", 26 | "Content-Length": "30", 27 | "Content-Type": "application/x-www-form-urlencoded", 28 | "Host": "httpbin.org", 29 | "User-Agent": "python-requests/2.31.0", 30 | "X-Amzn-Trace-Id": "Root=1-123abc-123abc", 31 | }, 32 | "json": None, 33 | "origin": "86.14.44.233", 34 | "url": "https://httpbin.org/post", 35 | } 36 | """ 37 | -------------------------------------------------------------------------------- /ch08/io_examples/string_io.py: -------------------------------------------------------------------------------- 1 | # io_examples/string_io.py 2 | import io 3 | 4 | 5 | stream = io.StringIO() 6 | stream.write("Learning Python Programming.\n") 7 | print("Become a Python ninja!", file=stream) 8 | 9 | contents = stream.getvalue() 10 | print(contents) 11 | 12 | stream.close() 13 | 14 | 15 | # better alternative, using a context manager 16 | with io.StringIO() as stream: 17 | stream.write("Learning Python Programming.\n") 18 | print("Become a Python ninja!", file=stream) 19 | 20 | contents = stream.getvalue() 21 | print(contents) 22 | 23 | 24 | """ 25 | $ python string_io.py 26 | Learning Python Programming. 27 | Become a Python ninja! 28 | 29 | Learning Python Programming. 30 | Become a Python ninja! 31 | """ 32 | -------------------------------------------------------------------------------- /ch08/json_examples/json_basic.py: -------------------------------------------------------------------------------- 1 | # json_examples/json_basic.py 2 | import sys 3 | import json 4 | 5 | 6 | data = { 7 | "big_number": 2**3141, 8 | "max_float": sys.float_info.max, 9 | "a_list": [2, 3, 5, 7], 10 | } 11 | 12 | 13 | json_data = json.dumps(data) 14 | data_out = json.loads(json_data) 15 | 16 | assert data == data_out # json and back, data matches 17 | 18 | 19 | # let us see how passing indent affects dumps. 20 | 21 | info = { 22 | "full_name": "Sherlock Holmes", 23 | "address": { 24 | "street": "221B Baker St", 25 | "zip": "NW1 6XE", 26 | "city": "London", 27 | "country": "UK", 28 | }, 29 | } 30 | 31 | 32 | print(json.dumps(info, indent=2, sort_keys=True)) 33 | 34 | 35 | """ 36 | $ python json_basic.py 37 | 38 | { 39 | "address": { 40 | "city": "London", 41 | "country": "UK", 42 | "street": "221B Baker St", 43 | "zip": "NW1 6XE" 44 | }, 45 | "full_name": "Sherlock Holmes" 46 | } 47 | """ 48 | -------------------------------------------------------------------------------- /ch08/json_examples/json_cplx.py: -------------------------------------------------------------------------------- 1 | # json_examples/json_cplx.py 2 | # Exercise: do the same for Fraction and Decimal 3 | import json 4 | 5 | 6 | class ComplexEncoder(json.JSONEncoder): 7 | def default(self, obj): 8 | print(f"ComplexEncoder.default: {obj=}") 9 | if isinstance(obj, complex): 10 | return { 11 | "_meta": "complex", 12 | "num": [obj.real, obj.imag], 13 | } 14 | return super().default(obj) 15 | 16 | 17 | data = { 18 | "an_int": 42, 19 | "a_float": 3.14159265, 20 | "a_complex": 3 + 4j, 21 | } 22 | 23 | json_data = json.dumps(data, cls=ComplexEncoder) 24 | 25 | print(json_data) 26 | 27 | 28 | def object_hook(obj): 29 | print(f"object_hook: {obj=}") 30 | try: 31 | if obj["_meta"] == "complex": 32 | return complex(*obj["num"]) 33 | except KeyError: 34 | return obj 35 | 36 | 37 | data_out = json.loads(json_data, object_hook=object_hook) 38 | print(data_out) 39 | 40 | 41 | """ 42 | $ python json_cplx.py 43 | 44 | ComplexEncoder.default: obj=(3+4j) 45 | { 46 | "an_int": 42, "a_float": 3.14159265, 47 | "a_complex": {"_meta": "complex", "num": [3.0, 4.0]} 48 | } 49 | object_hook: 50 | obj={'_meta': 'complex', 'num': [3.0, 4.0]} 51 | object_hook: 52 | obj={'an_int': 42, 'a_float': 3.14159265, 'a_complex': (3+4j)} 53 | {'an_int': 42, 'a_float': 3.14159265, 'a_complex': (3+4j)} 54 | """ 55 | -------------------------------------------------------------------------------- /ch08/json_examples/json_tuple.py: -------------------------------------------------------------------------------- 1 | # json_examples/json_tuple.py 2 | import json 3 | 4 | 5 | data_in = { 6 | "a_tuple": (1, 2, 3, 4, 5), 7 | } 8 | 9 | 10 | json_data = json.dumps(data_in) 11 | print(json_data) # {"a_tuple": [1, 2, 3, 4, 5]} 12 | data_out = json.loads(json_data) 13 | print(data_out) # {'a_tuple': [1, 2, 3, 4, 5]} 14 | -------------------------------------------------------------------------------- /ch08/persistence/alchemy_models.py: -------------------------------------------------------------------------------- 1 | # persistence/alchemy_models.py 2 | from sqlalchemy import ForeignKey, String, Integer 3 | from sqlalchemy.orm import ( 4 | DeclarativeBase, 5 | mapped_column, 6 | relationship, 7 | ) 8 | 9 | 10 | class Base(DeclarativeBase): 11 | pass 12 | 13 | 14 | class Person(Base): 15 | __tablename__ = "person" 16 | 17 | id = mapped_column(Integer, primary_key=True) 18 | name = mapped_column(String) 19 | age = mapped_column(Integer) 20 | 21 | emails = relationship( 22 | "Email", 23 | back_populates="person", 24 | order_by="Email.email", 25 | cascade="all, delete-orphan", 26 | ) 27 | 28 | def __repr__(self): 29 | return f"{self.name}(id={self.id})" 30 | 31 | 32 | class Email(Base): 33 | __tablename__ = "email" 34 | 35 | id = mapped_column(Integer, primary_key=True) 36 | email = mapped_column(String) 37 | person_id = mapped_column(ForeignKey("person.id")) 38 | person = relationship("Person", back_populates="emails") 39 | 40 | def __str__(self): 41 | return self.email 42 | 43 | __repr__ = __str__ 44 | -------------------------------------------------------------------------------- /ch08/persistence/pickler.py: -------------------------------------------------------------------------------- 1 | # persistence/pickler.py 2 | import pickle 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass 7 | class Person: 8 | first_name: str 9 | last_name: str 10 | id: int 11 | 12 | def greet(self): 13 | print( 14 | f"Hi, I am {self.first_name} {self.last_name}" 15 | f" and my ID is {self.id}" 16 | ) 17 | 18 | 19 | people = [ 20 | Person("Obi-Wan", "Kenobi", 123), 21 | Person("Anakin", "Skywalker", 456), 22 | ] 23 | 24 | 25 | # save data in binary format to a file 26 | with open("data.pickle", "wb") as stream: 27 | pickle.dump(people, stream) 28 | 29 | 30 | # load data from a file 31 | with open("data.pickle", "rb") as stream: 32 | peeps = pickle.load(stream) 33 | 34 | 35 | for person in peeps: 36 | person.greet() 37 | 38 | 39 | """ 40 | $ python pickler.py 41 | Hi, I am Obi-Wan Kenobi and my ID is 123 42 | Hi, I am Anakin Skywalker and my ID is 456 43 | """ 44 | -------------------------------------------------------------------------------- /ch08/persistence/shelf.py: -------------------------------------------------------------------------------- 1 | # persistence/shelf.py 2 | import shelve 3 | 4 | 5 | class Person: 6 | def __init__(self, name, id): 7 | self.name = name 8 | self.id = id 9 | 10 | 11 | with shelve.open("shelf1.shelve") as db: 12 | db["obi1"] = Person("Obi-Wan", 123) 13 | db["ani"] = Person("Anakin", 456) 14 | db["a_list"] = [2, 3, 5] 15 | db["delete_me"] = "we will have to delete this one..." 16 | 17 | print( 18 | list(db.keys()) 19 | ) # ['ani', 'delete_me', 'a_list', 'obi1'] 20 | 21 | del db["delete_me"] # gone! 22 | 23 | print(list(db.keys())) # ['ani', 'a_list', 'obi1'] 24 | 25 | print("delete_me" in db) # False 26 | print("ani" in db) # True 27 | 28 | a_list = db["a_list"] 29 | a_list.append(7) 30 | db["a_list"] = a_list 31 | 32 | print(db["a_list"]) # [2, 3, 5, 7] 33 | 34 | 35 | # this way allows writeback: 36 | # working with lists is easier, but consumes more memory and 37 | # closing the file takes longer. 38 | with shelve.open("shelf2.shelve", writeback=True) as db: 39 | db["a_list"] = [11, 13, 17] 40 | db["a_list"].append(19) # in-place append! 41 | 42 | print(db["a_list"]) # [11, 13, 17, 19] 43 | -------------------------------------------------------------------------------- /ch08/requirements/requirements.in: -------------------------------------------------------------------------------- 1 | requests 2 | sqlalchemy 3 | -------------------------------------------------------------------------------- /ch08/requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | certifi==2024.8.30 8 | # via requests 9 | charset-normalizer==3.4.0 10 | # via requests 11 | idna==3.10 12 | # via requests 13 | requests==2.32.3 14 | # via -r requirements.in 15 | sqlalchemy==2.0.36 16 | # via -r requirements.in 17 | typing-extensions==4.12.2 18 | # via sqlalchemy 19 | urllib3==2.2.3 20 | # via requests 21 | -------------------------------------------------------------------------------- /ch09/hmc.py: -------------------------------------------------------------------------------- 1 | # hmc.py 2 | import hmac 3 | import hashlib 4 | 5 | 6 | def calc_digest(key, message): 7 | key = bytes(key, "utf-8") 8 | message = bytes(message, "utf-8") 9 | 10 | dig = hmac.new(key, message, hashlib.sha256) 11 | return dig.hexdigest() 12 | 13 | 14 | mac = calc_digest("secret-key", "Important Message") 15 | print(mac) 16 | # 1db5d806d73d76e779b7fd31091e026362a64177368e82e0cab91e0c2fb6435e 17 | -------------------------------------------------------------------------------- /ch09/jwt/claims_time.py: -------------------------------------------------------------------------------- 1 | # jwt/claims_time.py 2 | from datetime import datetime, timedelta, UTC 3 | from time import sleep, time 4 | 5 | import jwt 6 | 7 | 8 | iat = datetime.now(tz=UTC) 9 | nfb = iat + timedelta(seconds=1) 10 | exp = iat + timedelta(seconds=3) 11 | 12 | 13 | data = {"payload": "data", "nbf": nfb, "exp": exp, "iat": iat} 14 | 15 | 16 | def decode(token, secret): 17 | print(f"{time():.2f}") 18 | try: 19 | print(jwt.decode(token, secret, algorithms=["HS256"])) 20 | except ( 21 | jwt.ImmatureSignatureError, 22 | jwt.ExpiredSignatureError, 23 | ) as err: 24 | print(err) 25 | print(type(err)) 26 | 27 | 28 | secret = "secret-key" 29 | token = jwt.encode(data, secret) 30 | 31 | 32 | decode(token, secret) 33 | sleep(2) 34 | decode(token, secret) 35 | sleep(2) 36 | decode(token, secret) 37 | 38 | 39 | """ 40 | $ python jwt/claims_time.py 41 | 1716674892.39 42 | The token is not yet valid (nbf) 43 | 44 | 45 | 1716674894.39 46 | {'payload': 'data', 'nbf': 1716674893, 'exp': 1716674895, 'iat': 1716674892} 47 | 48 | 1716674896.39 49 | Signature has expired 50 | 51 | """ 52 | -------------------------------------------------------------------------------- /ch09/jwt/rsa/key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDAU/AS173ll9rtzAyH8Fg0eS2hLtAJUxzG1Ym0//RoFJZUjTrwrDabvwVDoIikIuMRqpzASwnan49+FmdEyD6ptgfBgZTpDSOBuC50tgwdJ9c/1P1jCtXRBlmvFj3isvjR5fsBID2n7tdhSo4+S27vKzgtrcPynFHm8Uzz/LbagPm2S+bn313XJMjRPZQrasuFBkfJCuzWY4OBQqkkWkteA51iWZ4tbAGT8wA5lT18qU+Ttj3pty4WbLH7ZF88ivnaZJT5RFH6mQlCxtPE1NHkTsw9sDRONbZiqFz01OuzlLbmtzMaH0SGmNfurqY3ivMGhie4QZUHjKtT3RDqnV0ue/TuKABfyH6hn0Fy6QIYyity/q0E+vU02UJ/JcaBKWKehagmp7YV/uc/U/ctZItpbRFEDOY2tSCLIOlMm9S+h40JPWOhF8KYJO8ngJRPvwSVe3qewHI9LNpy1RHGHjl7EbJDWmVAsQUyMX1Gn7RoadzFjfLaymtWRdaUb7/46Lk= fab@fab-m1-pro.local 2 | -------------------------------------------------------------------------------- /ch09/jwt/tok.py: -------------------------------------------------------------------------------- 1 | # jwt/tok.py 2 | import jwt 3 | 4 | 5 | data = {"payload": "data", "id": 123456789} 6 | algs = ["HS256", "HS512"] 7 | 8 | token = jwt.encode(data, "secret-key") 9 | data_out = jwt.decode(token, "secret-key", algorithms=algs) 10 | print(token) 11 | print(data_out) 12 | 13 | 14 | # Decode without verifying the signature 15 | jwt.decode(token, options={"verify_signature": False}) 16 | 17 | 18 | # Let's use another algorithm 19 | token512 = jwt.encode(data, "secret-key", algorithm="HS512") 20 | data_out = jwt.decode( 21 | token512, "secret-key", algorithms=["HS512"] 22 | ) 23 | print(data_out) 24 | 25 | """ 26 | $ python jwt/tok.py 27 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXlsb2FkIjoiZGF0YSIsIm... 28 | {'payload': 'data', 'id': 123456789} 29 | {'payload': 'data', 'id': 123456789} 30 | """ 31 | -------------------------------------------------------------------------------- /ch09/jwt/token_rsa.py: -------------------------------------------------------------------------------- 1 | # jwt/token_rsa.py 2 | import jwt 3 | 4 | 5 | data = {"payload": "data"} 6 | 7 | 8 | def encode(data, priv_filename, algorithm="RS256"): 9 | 10 | with open(priv_filename, "rb") as key: 11 | private_key = key.read() 12 | 13 | return jwt.encode(data, private_key, algorithm=algorithm) 14 | 15 | 16 | def decode(data, pub_filename, algorithm="RS256"): 17 | 18 | with open(pub_filename, "rb") as key: 19 | public_key = key.read() 20 | 21 | return jwt.decode(data, public_key, algorithms=[algorithm]) 22 | 23 | 24 | token = encode(data, "jwt/rsa/key") 25 | data_out = decode(token, "jwt/rsa/key.pub") 26 | print(data_out) # {'payload': 'data'} 27 | -------------------------------------------------------------------------------- /ch09/requirements/requirements.in: -------------------------------------------------------------------------------- 1 | pyjwt 2 | cryptography 3 | -------------------------------------------------------------------------------- /ch09/requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | cffi==1.17.1 8 | # via cryptography 9 | cryptography==43.0.3 10 | # via -r requirements.in 11 | pycparser==2.22 12 | # via cffi 13 | pyjwt==2.10.1 14 | # via -r requirements.in 15 | -------------------------------------------------------------------------------- /ch09/secrs/secr_gen.py: -------------------------------------------------------------------------------- 1 | # secrs/secr_gen.py 2 | import secrets 3 | from string import digits, ascii_letters 4 | 5 | 6 | def generate_pwd(length=8): 7 | chars = digits + ascii_letters 8 | return "".join(secrets.choice(chars) for c in range(length)) 9 | 10 | 11 | def generate_secure_pwd(length=16, upper=3, digits=3): 12 | if length < upper + digits + 1: 13 | raise ValueError("Nice try!") 14 | 15 | while True: 16 | pwd = generate_pwd(length) 17 | if ( 18 | any(c.islower() for c in pwd) 19 | and sum(c.isupper() for c in pwd) >= upper 20 | and sum(c.isdigit() for c in pwd) >= digits 21 | ): 22 | return pwd 23 | 24 | 25 | print(generate_secure_pwd()) 26 | print(generate_secure_pwd(length=3, upper=1, digits=1)) 27 | 28 | """ 29 | $ python secr_gen.py 30 | mgQ3Hj57KjD1LI7M 31 | b8G 32 | """ 33 | -------------------------------------------------------------------------------- /ch09/secrs/secr_rand.py: -------------------------------------------------------------------------------- 1 | # secrs/secr_rand.py 2 | import secrets 3 | 4 | 5 | # Utils 6 | print(secrets.choice("Choose one of these words".split())) 7 | 8 | print(secrets.randbelow(10**6)) 9 | 10 | print(secrets.randbits(32)) 11 | 12 | 13 | # Tokens 14 | print(secrets.token_bytes(16)) 15 | 16 | print(secrets.token_hex(32)) 17 | 18 | print(secrets.token_urlsafe(32)) 19 | 20 | # Compare digests against timing attacks 21 | secrets.compare_digest("abc123", "abc123") 22 | 23 | """ 24 | $ python secr_rand.py 25 | one 26 | 133025 27 | 1509555468 28 | b'\x0f\x8b\x8f\x0f\xe3\xceJ\xbc\x18\xf2\x1e\xe0i\xee1\x99' 29 | 98e80cddf6c371811318045672399b0950b8e3207d18b50d99d724d31d17f0a7 30 | 63eNkRalj8dgZqmkezjbEYoGddVcutgvwJthSLf5kho 31 | """ 32 | -------------------------------------------------------------------------------- /ch09/secrs/secr_reset.py: -------------------------------------------------------------------------------- 1 | # secrs/secr_reset.py 2 | import secrets 3 | 4 | 5 | def get_reset_pwd_url(token_length=16): 6 | token = secrets.token_urlsafe(token_length) 7 | return f"https://example.com/reset-pwd/{token}" 8 | 9 | 10 | print(get_reset_pwd_url()) 11 | 12 | 13 | """ 14 | $ python secr_reset.py 15 | https://example.com/reset-pwd/ML_6_2wxDpXmDJLHrDnrRA 16 | """ 17 | -------------------------------------------------------------------------------- /ch10/data.py: -------------------------------------------------------------------------------- 1 | # data.py 2 | def get_clean_data(source): 3 | 4 | data = load_data(source) 5 | cleaned_data = clean_data(data) 6 | 7 | return cleaned_data 8 | -------------------------------------------------------------------------------- /ch10/first.run.failure.txt: -------------------------------------------------------------------------------- 1 | $ pytest tests -vv 2 | ===================== test session starts ===================== 3 | platform darwin -- Python 3.12.2, pytest-8.1.1, pluggy-1.4.0 -- 4 | /Users/fab/.virtualenvs/lpp4ed-ch10/bin/python 5 | cachedir: .pytest_cache 6 | rootdir: /Users/fab/code/lpp4ed 7 | configfile: pyproject.toml 8 | collected 2 items 9 | 10 | tests/test_api.py::TestIsValid::test_minimal FAILED [ 50%] 11 | tests/test_api.py::TestIsValid::test_full PASSED [100%] 12 | 13 | =========================== FAILURES ========================== 14 | ___________________ TestIsValid.test_minimal __________________ 15 | 16 | self = , 17 | min_user = {'age': 18, 'email': 'minimal@example.com'} 18 | 19 | def test_minimal(self, min_user): 20 | > assert is_valid(min_user) 21 | E AssertionError: assert False 22 | E + where False = is_valid( 23 | {'age': 18, 'email': 'minimal@example.com'} 24 | ) 25 | 26 | tests/test_api.py:45: AssertionError 27 | =================== short test summary info =================== 28 | FAILED tests/test_api.py::TestIsValid::test_minimal 29 | - AssertionError: assert False 30 | ================= 1 failed, 1 passed in 0.04s ================= 31 | -------------------------------------------------------------------------------- /ch10/first.run.txt: -------------------------------------------------------------------------------- 1 | $ pytest tests -vv 2 | ===================== test session starts ===================== 3 | platform darwin -- Python 3.12.2, pytest-8.1.1, pluggy-1.4.0 -- 4 | /Users/fab/.virtualenvs/lpp4ed-ch10/bin/python 5 | cachedir: .pytest_cache 6 | rootdir: /Users/fab/code/lpp4ed 7 | configfile: pyproject.toml 8 | collected 2 items 9 | 10 | tests/test_api.py::TestIsValid::test_minimal PASSED [ 50%] 11 | tests/test_api.py::TestIsValid::test_full PASSED [100%] 12 | 13 | ====================== 2 passed in 0.03s ====================== 14 | -------------------------------------------------------------------------------- /ch10/full.run.txt: -------------------------------------------------------------------------------- 1 | $ pytest tests 2 | ====================== test session starts ====================== 3 | platform darwin -- Python 3.12.2, pytest-8.1.1, pluggy-1.4.0 4 | rootdir: /Users/fab/code/lpp4ed 5 | configfile: pyproject.toml 6 | collected 132 items 7 | 8 | tests/test_api.py .............................................. 9 | ................................................................ 10 | ...................... [100%] 11 | 12 | ====================== 132 passed in 0.14s ====================== 13 | -------------------------------------------------------------------------------- /ch10/requirements/requirements.in: -------------------------------------------------------------------------------- 1 | marshmallow 2 | pytest 3 | -------------------------------------------------------------------------------- /ch10/requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | iniconfig==2.0.0 8 | # via pytest 9 | marshmallow==3.23.1 10 | # via -r requirements.in 11 | packaging==24.2 12 | # via 13 | # marshmallow 14 | # pytest 15 | pluggy==1.5.0 16 | # via pytest 17 | pytest==8.3.3 18 | # via -r requirements.in 19 | -------------------------------------------------------------------------------- /ch10/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # tests/__init__.py 2 | -------------------------------------------------------------------------------- /ch11/assertions.py: -------------------------------------------------------------------------------- 1 | # assertions.py 2 | mylist = [1, 2, 3] # pretend this comes from an external source 3 | 4 | assert 4 == len(mylist) # this will break 5 | 6 | for position in range(4): 7 | print(mylist[position]) 8 | 9 | 10 | """ 11 | Traceback (most recent call last): 12 | File ".../ch11/assertions.py", line 4, in 13 | assert 4 == len(mylist) # this will break 14 | ^^^^^^^^^^^^^^^^ 15 | AssertionError 16 | """ 17 | 18 | # asserts with message 19 | assert 4 == len(mylist), f"Mylist has {len(mylist)} elements" 20 | 21 | 22 | """ 23 | Traceback (most recent call last): 24 | File ".../ch11/assertions.py", line 19, in 25 | assert 4 == len(mylist), f"Mylist has {len(mylist)} elements" 26 | ^^^^^^^^^^^^^^^^ 27 | AssertionError: Mylist has 3 elements 28 | """ 29 | -------------------------------------------------------------------------------- /ch11/custom.py: -------------------------------------------------------------------------------- 1 | # custom.py 2 | def debug(*msg, print_separator=True): 3 | print(*msg) 4 | if print_separator: 5 | print("-" * 40) 6 | 7 | 8 | debug("Data is ...") 9 | debug("Different", "Strings", "Are not a problem") 10 | debug("After while loop", print_separator=False) 11 | 12 | 13 | """ 14 | $ python custom.py 15 | Data is ... 16 | ---------------------------------------- 17 | Different Strings Are not a problem 18 | ---------------------------------------- 19 | After while loop 20 | """ 21 | -------------------------------------------------------------------------------- /ch11/custom_timestamp.py: -------------------------------------------------------------------------------- 1 | # custom_timestamp.py 2 | from time import sleep 3 | 4 | 5 | def debug(*msg, timestamp=[None]): 6 | from time import time # local import 7 | 8 | print(*msg) 9 | 10 | if timestamp[0] is None: 11 | timestamp[0] = time() # 1 12 | else: 13 | now = time() 14 | print(f" Time elapsed: {now - timestamp[0]:.3f}s") 15 | timestamp[0] = now # 2 16 | 17 | 18 | debug("Entering buggy piece of code...") 19 | sleep(0.3) 20 | debug("First step done.") 21 | sleep(0.5) 22 | debug("Second step done.") 23 | 24 | 25 | """ 26 | $ python custom_timestamp.py 27 | Entering buggy piece of code... 28 | First step done. 29 | Time elapsed: 0.300s 30 | Second step done. 31 | Time elapsed: 0.500s 32 | """ 33 | -------------------------------------------------------------------------------- /ch11/log.py: -------------------------------------------------------------------------------- 1 | # log.py 2 | import logging 3 | 4 | 5 | logging.basicConfig( 6 | filename="ch11.log", 7 | level=logging.DEBUG, 8 | format="[%(asctime)s] %(levelname)s: %(message)s", 9 | datefmt="%m/%d/%Y %I:%M:%S %p", 10 | ) 11 | 12 | mylist = [1, 2, 3] 13 | logging.info("Starting to process 'mylist'...") 14 | 15 | for position in range(4): 16 | try: 17 | logging.debug( 18 | "Value at position %s is %s", 19 | position, 20 | mylist[position], 21 | ) 22 | except IndexError: 23 | logging.exception("Faulty position: %s", position) 24 | 25 | logging.info("Done processing 'mylist'.") 26 | -------------------------------------------------------------------------------- /ch11/pdebugger.py: -------------------------------------------------------------------------------- 1 | # pdebugger.py 2 | # d comes from a JSON payload we don't control 3 | d = {"first": "v1", "second": "v2", "fourth": "v4"} 4 | # keys also comes from a JSON payload we don't control 5 | keys = ("first", "second", "third", "fourth") 6 | 7 | 8 | def do_something_with_value(value): 9 | print(value) 10 | 11 | 12 | for key in keys: 13 | do_something_with_value(d[key]) 14 | 15 | print("Validation done.") 16 | 17 | """ 18 | $ python pdebugger.py 19 | v1 20 | v2 21 | Traceback (most recent call last): 22 | File ".../ch11/pdebugger.py", line 13, in 23 | do_something_with_value(d[key]) 24 | ~^^^^^ 25 | KeyError: 'third' 26 | """ 27 | -------------------------------------------------------------------------------- /ch11/pdebugger_pdb.py: -------------------------------------------------------------------------------- 1 | # pdebugger_pdb.py 2 | # d comes from a JSON payload we don't control 3 | d = {"first": "v1", "second": "v2", "fourth": "v4"} 4 | # keys also comes from a JSON payload we don't control 5 | keys = ("first", "second", "third", "fourth") 6 | 7 | 8 | def do_something_with_value(value): 9 | print(value) 10 | 11 | 12 | breakpoint() 13 | 14 | # or: 15 | # import pdb; pdb.set_trace() 16 | 17 | for key in keys: 18 | do_something_with_value(d[key]) 19 | 20 | print("Validation done.") 21 | 22 | 23 | """ 24 | $ python pdebugger_pdb.py 25 | [0] > .../ch11/pdebugger_pdb.py(17)() 26 | -> for key in keys: 27 | (Pdb++) l 28 | 12 breakpoint() 29 | 13 30 | 14 # or: 31 | 15 # import pdb; pdb.set_trace() 32 | 16 33 | 17 -> for key in keys: 34 | 18 do_something_with_value(d[key]) 35 | 19 36 | 20 print("Validation done.") 37 | 21 38 | 22 39 | (Pdb++) keys # inspect the keys tuple 40 | ('first', 'second', 'third', 'fourth') 41 | (Pdb++) d.keys() # inspect keys of d 42 | dict_keys(['first', 'second', 'fourth']) 43 | (Pdb++) d['third'] = 'placeholder' # add missing item 44 | (Pdb++) c # continue 45 | v1 46 | v2 47 | placeholder 48 | v4 49 | Validation done. 50 | """ 51 | -------------------------------------------------------------------------------- /ch11/profiling/triples.py: -------------------------------------------------------------------------------- 1 | # profiling/triples.py 2 | def calc_triples(mx): 3 | triples = [] 4 | for a in range(1, mx + 1): 5 | for b in range(a, mx + 1): 6 | hypotenuse = calc_hypotenuse(a, b) 7 | if is_int(hypotenuse): 8 | triples.append((a, b, int(hypotenuse))) 9 | return triples 10 | 11 | 12 | def calc_hypotenuse(a, b): 13 | return (a**2 + b**2) ** 0.5 14 | 15 | 16 | def is_int(n): 17 | return n.is_integer() 18 | 19 | 20 | triples = calc_triples(1000) 21 | 22 | """ 23 | $ python -m cProfile profiling/triples.py 24 | 1502538 function calls in 0.393 seconds 25 | 26 | Ordered by: cumulative time 27 | 28 | ncalls tottime percall cumtime percall filename:lineno(function) 29 | 1 0.000 0.000 0.393 0.393 {built-in method builtins.exec} 30 | 1 0.000 0.000 0.393 0.393 triples.py:1() 31 | 1 0.143 0.143 0.393 0.393 triples.py:1(calc_triples) 32 | 500500 0.087 0.000 0.147 0.000 triples.py:15(is_int) 33 | 500500 0.102 0.000 0.102 0.000 triples.py:11(calc_hypotenuse) 34 | 500500 0.060 0.000 0.060 0.000 {method 'is_integer' of 'float' objects} 35 | 1034 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects} 36 | 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} 37 | """ 38 | -------------------------------------------------------------------------------- /ch11/profiling/triples_v2.py: -------------------------------------------------------------------------------- 1 | # profiling/triples_v2.py 2 | def calc_triples(mx): 3 | triples = [] 4 | for a in range(1, mx + 1): 5 | for b in range(a, mx + 1): 6 | hypotenuse = calc_hypotenuse(a, b) 7 | if hypotenuse.is_integer(): 8 | triples.append((a, b, int(hypotenuse))) 9 | return triples 10 | 11 | 12 | def calc_hypotenuse(a, b): 13 | return (a**2 + b**2) ** 0.5 14 | 15 | 16 | triples = calc_triples(1000) 17 | 18 | """ 19 | $ python -m cProfile profiling/triples_v2.py 20 | 1002038 function calls in 0.304 seconds 21 | 22 | Ordered by: cumulative time 23 | 24 | ncalls tottime percall cumtime percall filename:lineno(function) 25 | 1 0.000 0.000 0.304 0.304 {built-in method builtins.exec} 26 | 1 0.000 0.000 0.304 0.304 triples_v2.py:1() 27 | 1 0.143 0.143 0.304 0.304 triples_v2.py:1(calc_triples) 28 | 500500 0.101 0.000 0.101 0.000 triples_v2.py:11(calc_hypotenuse) 29 | 500500 0.060 0.000 0.060 0.000 {method 'is_integer' of 'float' objects} 30 | 1034 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects} 31 | 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} 32 | """ 33 | -------------------------------------------------------------------------------- /ch11/profiling/triples_v3.py: -------------------------------------------------------------------------------- 1 | # profiling/triples_v3.py 2 | def calc_triples(mx): 3 | triples = [] 4 | for a in range(1, mx + 1): 5 | for b in range(a, mx + 1): 6 | hypotenuse = calc_hypotenuse(a, b) 7 | if hypotenuse.is_integer(): 8 | triples.append((a, b, int(hypotenuse))) 9 | return triples 10 | 11 | 12 | def calc_hypotenuse(a, b): 13 | return (a * a + b * b) ** 0.5 14 | 15 | 16 | triples = calc_triples(1000) 17 | 18 | """ 19 | $ python -m cProfile profiling/triples_v3.py 20 | 1002038 function calls in 0.287 seconds 21 | 22 | Ordered by: cumulative time 23 | 24 | ncalls tottime percall cumtime percall filename:lineno(function) 25 | 1 0.000 0.000 0.287 0.287 {built-in method builtins.exec} 26 | 1 0.000 0.000 0.287 0.287 triples_v3.py:1() 27 | 1 0.143 0.143 0.287 0.287 triples_v3.py:1(calc_triples) 28 | 500500 0.084 0.000 0.084 0.000 triples_v3.py:11(calc_hypotenuse) 29 | 500500 0.060 0.000 0.060 0.000 {method 'is_integer' of 'float' objects} 30 | 1034 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects} 31 | 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} 32 | """ 33 | -------------------------------------------------------------------------------- /ch11/profiling/triples_v4.py: -------------------------------------------------------------------------------- 1 | # profiling/triples_v4.py 2 | def calc_triples(mx): 3 | triples = [] 4 | for a in range(1, mx + 1): 5 | for b in range(a, mx + 1): 6 | hypotenuse = (a * a + b * b) ** 0.5 7 | if hypotenuse.is_integer(): 8 | triples.append((a, b, int(hypotenuse))) 9 | return triples 10 | 11 | 12 | triples = calc_triples(1000) 13 | 14 | """ 15 | $ python -m cProfile profiling/triples_v4.py 16 | 501538 function calls in 0.186 seconds 17 | 18 | Ordered by: cumulative time 19 | 20 | ncalls tottime percall cumtime percall filename:lineno(function) 21 | 1 0.000 0.000 0.186 0.186 {built-in method builtins.exec} 22 | 1 0.000 0.000 0.186 0.186 triples_v4.py:1() 23 | 1 0.128 0.128 0.186 0.186 triples_v4.py:1(calc_triples) 24 | 500500 0.058 0.000 0.058 0.000 {method 'is_integer' of 'float' objects} 25 | 1034 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects} 26 | 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} 27 | """ 28 | -------------------------------------------------------------------------------- /ch11/requirements/requirements.in: -------------------------------------------------------------------------------- 1 | pdbpp 2 | -------------------------------------------------------------------------------- /ch11/requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | attrs==24.2.0 8 | # via wmctrl 9 | fancycompleter==0.9.1 10 | # via pdbpp 11 | pdbpp==0.10.3 12 | # via -r requirements.in 13 | pygments==2.18.0 14 | # via pdbpp 15 | pyrepl==0.9.0 16 | # via fancycompleter 17 | wmctrl==0.5 18 | # via pdbpp 19 | -------------------------------------------------------------------------------- /ch12/annotations/any.py: -------------------------------------------------------------------------------- 1 | # annotations/any.py 2 | from typing import Any 3 | 4 | 5 | # Equivalent definitions of the square function 6 | def square(n): 7 | return n**2 8 | 9 | 10 | def square_annotated(n: Any) -> Any: 11 | return n**2 12 | 13 | 14 | # Equivalent definitions of the reverse function 15 | def reverse(items: list) -> list: 16 | return list(reversed(items)) 17 | 18 | 19 | def reverse_any(items: list[Any]) -> list[Any]: 20 | return list(reversed(items)) 21 | 22 | 23 | if __name__ == "__main__": 24 | print(square(5)) # 25 25 | print(square_annotated(5)) # 25 26 | print(reverse([1, 2, 3])) # [3, 2, 1] 27 | print(reverse_any([1, 2, 3])) # [3, 2, 1] 28 | -------------------------------------------------------------------------------- /ch12/annotations/basic.py: -------------------------------------------------------------------------------- 1 | # annotations/basic.py 2 | 3 | 4 | def greeter(name): 5 | return f"Hello, {name}!" 6 | 7 | 8 | def greeter_annotated(name: str) -> str: 9 | return f"Hello, {name}!" 10 | 11 | 12 | def greeter_annotated_age(name: str, age: int) -> str: 13 | return f"Hello, {name}! You are {age} years old." 14 | 15 | 16 | if __name__ == "__main__": 17 | print(greeter("Alice")) 18 | print(greeter_annotated("Bob")) 19 | print(greeter_annotated_age("Charlie", 30)) 20 | -------------------------------------------------------------------------------- /ch12/annotations/collections.abc.iterable.py: -------------------------------------------------------------------------------- 1 | # annotations/collections.abc.iterable.py 2 | 3 | from collections.abc import Iterable, Callable 4 | 5 | 6 | def process_items(items: Iterable) -> None: 7 | for item in items: 8 | print(item) 9 | 10 | 11 | def process_callback( 12 | arg: str, callback: Callable[[str], str] 13 | ) -> str: 14 | return callback(arg) 15 | 16 | 17 | def greeter(name: str) -> str: 18 | return f"Hello, {name}!" 19 | 20 | 21 | def reverse(name: str) -> str: 22 | return name[::-1] 23 | 24 | 25 | if __name__ == "__main__": 26 | print(process_callback("Alice", greeter)) # Hello, Alice! 27 | print(process_callback("Alice", reverse)) # ecilA 28 | -------------------------------------------------------------------------------- /ch12/annotations/collections.abcs.py: -------------------------------------------------------------------------------- 1 | # annotations/collections.abcs.py 2 | 3 | from collections.abc import Mapping, Sequence 4 | 5 | 6 | def average_bad(v: list[float]) -> float: 7 | return sum(v) / len(v) 8 | 9 | 10 | def average(v: Sequence[float]) -> float: 11 | return sum(v) / len(v) 12 | 13 | 14 | def greet_user_bad(user: dict[str, str]) -> str: 15 | return f"Hello, {user['name']}!" 16 | 17 | 18 | def greet_user(user: Mapping[str, str]) -> str: 19 | return f"Hello, {user['name']}!" 20 | 21 | 22 | def add_defaults_bad( 23 | data: Mapping[str, str] 24 | ) -> Mapping[str, str]: 25 | defaults = {"host": "localhost", "port": "5432"} 26 | return {**defaults, **data} 27 | 28 | 29 | def add_defaults(data: Mapping[str, str]) -> dict[str, str]: 30 | defaults = {"host": "localhost", "port": "5432"} 31 | return {**defaults, **data} 32 | 33 | 34 | if __name__ == "__main__": 35 | print(average_bad([1.0, 2.0, 3.0])) # 2.0 36 | print(average((1.0, 2.0, 3.0))) # 2.0 37 | 38 | print(greet_user_bad({"name": "Alice"})) # Hello, Alice! 39 | print(greet_user({"name": "Charlie"})) # Hello, Charlie! 40 | 41 | # {'host': 'db.example.com', 'port': 5432} 42 | print(add_defaults({"host": "db.example.com"})) 43 | print(add_defaults_bad({"host": "db.example.com"})) 44 | -------------------------------------------------------------------------------- /ch12/annotations/containers.py: -------------------------------------------------------------------------------- 1 | # annotations/containers.py 2 | 3 | # The type checker assumes all elements of the list are integers 4 | a: list[int] = [1, 2, 3] 5 | 6 | # We cannot specify two types for the elements of the list 7 | # it only accepts a single type argument 8 | b: list[int, str] = [1, 2, 3, "four"] # Wrong! 9 | 10 | # The type checker will infer that all keys in `c` are strings 11 | # and all values are integers or strings 12 | c: dict[str, int | str] = {"one": 1, "two": "2"} 13 | -------------------------------------------------------------------------------- /ch12/annotations/generics.py: -------------------------------------------------------------------------------- 1 | # annotations/generics.py 2 | from typing import TypeVar 3 | 4 | U = TypeVar("U") 5 | 6 | 7 | def last[T](items: list[T]) -> T | None: 8 | return items[-1] if items else None 9 | 10 | 11 | def first(items: list[U]) -> U | None: 12 | return items[0] if items else None 13 | 14 | 15 | if __name__ == "__main__": 16 | print(last([1, 2, 3])) # 3 17 | print(last([])) # None 18 | 19 | print(first([1, 2, 3])) # 1 20 | print(first([])) # None 21 | -------------------------------------------------------------------------------- /ch12/annotations/optional.py: -------------------------------------------------------------------------------- 1 | # annotations/optional.py 2 | from typing import Optional 3 | 4 | 5 | def greeter(name: str = "stranger") -> str: 6 | return f"Hello, {name}!" 7 | 8 | 9 | def greeter_optional(name: Optional[str] = None) -> str: 10 | if name is not None: 11 | return f"Hello, {name}!" 12 | return "No-one to greet!" 13 | 14 | 15 | def another_greeter(name: str | None = None) -> str: 16 | if name is not None: 17 | return f"Hello, {name}!" 18 | return "No-one to greet!" 19 | 20 | 21 | if __name__ == "__main__": 22 | print(greeter()) # Hello, stranger! 23 | print(greeter("Alice")) # Hello, Alice! 24 | 25 | print(greeter_optional()) # No-one to greet! 26 | print(greeter_optional("Bob")) # Hello, Bob! 27 | 28 | print(another_greeter()) # No-one to greet! 29 | print(another_greeter("Charlie")) # Hello, Charlie! 30 | -------------------------------------------------------------------------------- /ch12/annotations/protocols.py: -------------------------------------------------------------------------------- 1 | # annotations/protocols.py 2 | from typing import Iterable, Protocol 3 | 4 | 5 | class SupportsStart(Protocol): 6 | def start(self) -> None: ... 7 | 8 | 9 | class Worker: # No SupportsStart base class. 10 | def __init__(self, name: str) -> None: 11 | self.name = name 12 | 13 | def start(self) -> None: 14 | print(f"Starting worker {self.name}") 15 | 16 | 17 | def start_workers(workers: Iterable[SupportsStart]) -> None: 18 | for worker in workers: 19 | worker.start() 20 | 21 | 22 | workers = [Worker("Alice"), Worker("Bob")] 23 | start_workers(workers) 24 | # Starting worker Alice 25 | # Starting worker Bob 26 | -------------------------------------------------------------------------------- /ch12/annotations/protocols.subclassing.py: -------------------------------------------------------------------------------- 1 | # annotations/protocols.subclassing.py 2 | from typing import Iterable, Protocol 3 | 4 | 5 | class SupportsStart(Protocol): 6 | def start(self) -> None: ... 7 | 8 | 9 | class SupportsStop(Protocol): 10 | def stop(self) -> None: ... 11 | 12 | 13 | class SupportsWorkCycle(SupportsStart, SupportsStop, Protocol): 14 | pass 15 | 16 | 17 | class Worker: 18 | def __init__(self, name: str) -> None: 19 | self.name = name 20 | 21 | def start(self) -> None: 22 | print(f"Starting worker {self.name}") 23 | 24 | def stop(self) -> None: 25 | print(f"Stopping worker {self.name}") 26 | 27 | 28 | def start_workers(workers: Iterable[SupportsWorkCycle]) -> None: 29 | for worker in workers: 30 | worker.start() 31 | worker.stop() 32 | 33 | 34 | workers = [Worker("Alice"), Worker("Bob")] 35 | start_workers(workers) 36 | # Starting worker Alice 37 | # Stopping worker Alice 38 | # Starting worker Bob 39 | # Stopping worker Bob 40 | -------------------------------------------------------------------------------- /ch12/annotations/self.py: -------------------------------------------------------------------------------- 1 | # annotations/self.py 2 | from typing import Self 3 | from collections.abc import Iterable 4 | from dataclasses import dataclass 5 | 6 | 7 | @dataclass 8 | class Point: 9 | x: float = 0.0 10 | y: float = 0.0 11 | z: float = 0.0 12 | 13 | def magnitude(self) -> float: 14 | return (self.x**2 + self.y**2 + self.z**2) ** 0.5 15 | 16 | @classmethod 17 | def sum_points(cls, points: Iterable[Self]) -> Self: 18 | return cls( 19 | sum(p.x for p in points), 20 | sum(p.y for p in points), 21 | sum(p.z for p in points), 22 | ) 23 | 24 | 25 | if __name__ == "__main__": 26 | p1 = Point(1, 2, 3) 27 | p2 = Point(4, 5, 6) 28 | print(Point.sum_points([p1, p2])) # Point(x=5, y=7, z=9) 29 | -------------------------------------------------------------------------------- /ch12/annotations/tuples.any.length.py: -------------------------------------------------------------------------------- 1 | # annotations/tuples.any.length.py 2 | from typing import Any 3 | 4 | # We use the ellipsis to indicate that the tuple can have any 5 | # number of elements. 6 | a: tuple[int, ...] = (1, 2, 3) 7 | 8 | # All the following are valid, because the tuple can have any 9 | # number of elements. 10 | a = () 11 | a = (7,) 12 | a = (7, 8, 9, 10, 11) 13 | 14 | # But this is an error, because the tuple can only have integers 15 | a = ("hello", "world") 16 | 17 | # We can specify a tuple that must be empty 18 | b: tuple[()] = () 19 | 20 | # Finally, if we annotate a tuple like this: 21 | c: tuple = (1, 2, 3) 22 | # The type checker will treat it as if we had written: 23 | c: tuple[Any, ...] = (1, 2, 3) 24 | # And because of that, all the below are valid: 25 | c = () 26 | c = ("hello", "my", "friend") 27 | -------------------------------------------------------------------------------- /ch12/annotations/tuples.fixed.py: -------------------------------------------------------------------------------- 1 | # annotations/tuples.fixed.py 2 | 3 | # Tuple `a` is assigned to a tuple of length 1, 4 | # with a single string element. 5 | a: tuple[str] = ("hello",) 6 | 7 | # Tuple `b` is assigned to a tuple of length 2, 8 | # with an integer and a string element. 9 | b: tuple[int, str] = (1, "one") 10 | 11 | # Type checker error: the annotation indicates a tuple of 12 | # length 1, but the tuple has 3 elements. 13 | c: tuple[float] = (3.14, 1.0, -1.0) # Wrong! 14 | -------------------------------------------------------------------------------- /ch12/annotations/tuples.named.py: -------------------------------------------------------------------------------- 1 | # annotations/tuples.named.py 2 | 3 | from typing import NamedTuple 4 | 5 | 6 | class Person(NamedTuple): 7 | name: str 8 | age: int 9 | 10 | 11 | fab = Person("Fab", 48) 12 | print(fab) # Person(name='Fab', age=48) 13 | print(fab.name) # Fab 14 | print(fab.age) # 48 15 | print(fab[0]) # Fab 16 | print(fab[1]) # 48 17 | 18 | 19 | # The above is equivalent to: 20 | # import collections 21 | # Person = collections.namedtuple("Person", ["name", "age"]) 22 | 23 | 24 | class Point(NamedTuple): 25 | x: int 26 | y: int 27 | z: int = 0 28 | 29 | 30 | p = Point(1, 2) 31 | print(p) # Point(x=1, y=2, z=0) 32 | -------------------------------------------------------------------------------- /ch12/annotations/type.aliases.py: -------------------------------------------------------------------------------- 1 | # annotations/type.aliases.py 2 | 3 | type DatabasePort = int 4 | 5 | 6 | def connect_to_database(host: str, port: DatabasePort): 7 | print(f"Connecting to {host} on port {port}...") 8 | 9 | 10 | if __name__ == "__main__": 11 | connect_to_database("localhost", 5432) 12 | -------------------------------------------------------------------------------- /ch12/annotations/union.py: -------------------------------------------------------------------------------- 1 | # annotations/union.py 2 | from typing import Union 3 | 4 | 5 | def connect_to_database(host: str, port: Union[int, str]): 6 | print(f"Connecting to {host} on port {port}...") 7 | 8 | 9 | def connect_to_db(host: str, port: int | str): 10 | print(f"Connecting to {host} on port {port}...") 11 | 12 | 13 | if __name__ == "__main__": 14 | connect_to_database("localhost", 5432) 15 | connect_to_database("localhost", "5432") 16 | 17 | connect_to_db("localhost", 5432) 18 | connect_to_db("localhost", "5432") 19 | -------------------------------------------------------------------------------- /ch12/annotations/variable.parameters.py: -------------------------------------------------------------------------------- 1 | # annotations/variable.parameters.py 2 | 3 | 4 | def add_query_params( 5 | *urls: str, **query_params: str 6 | ) -> list[str]: 7 | params = "&".join(f"{k}={v}" for k, v in query_params.items()) 8 | return [f"{url}?{params}" for url in urls] 9 | 10 | 11 | urls = add_query_params( 12 | "https://example1.com", 13 | "https://example2.com", 14 | "https://example3.com", 15 | limit="10", 16 | offset="20", 17 | sort="desc", 18 | ) 19 | print(urls) 20 | # ['https://example1.com?limit=10&offset=20&sort=desc', 21 | # 'https://example2.com?limit=10&offset=20&sort=desc', 22 | # 'https://example3.com?limit=10&offset=20&sort=desc'] 23 | -------------------------------------------------------------------------------- /ch12/annotations/variables.py: -------------------------------------------------------------------------------- 1 | x: int = 10 2 | x: float = 3.14 3 | x: bool = True 4 | x: str = "Hello!" 5 | x: bytes = b"Hello!" 6 | 7 | # Python 3.9+ 8 | x: list[int] = [7, 14, 21] 9 | x: set[int] = {1, 2, 3} 10 | x: dict[str, float] = {"planck": 6.62607015e-34} 11 | 12 | x: tuple[int, str, float] = (1, "on", 2.8) 13 | x: tuple[int, ...] = (1, 2, 3) 14 | 15 | # Python 3.8 and earlier 16 | from typing import List, Set, Dict, Tuple 17 | 18 | x: List[int] = [7, 14, 21] 19 | x: Set[int] = {1, 2, 3} 20 | x: Dict[str, float] = {"planck": 6.62607015e-34} 21 | 22 | x: Tuple[int, str, float] = (1, "on", 2.8) 23 | x: Tuple[int, ...] = (1, 2, 3) 24 | 25 | # Python 3.10+ 26 | x: list[int | str] = [0, 1, 1, 2, "fibonacci", "rules"] 27 | 28 | # Python 3.9 and earlier 29 | from typing import Union 30 | 31 | x: list[Union[int, str]] = [0, 1, 1, 2, "fibonacci", "rules"] 32 | -------------------------------------------------------------------------------- /ch12/duck.typing.py: -------------------------------------------------------------------------------- 1 | # duck.typing.py 2 | 3 | 4 | class Circle: 5 | def __init__(self, radius): 6 | self.radius = radius 7 | 8 | def area(self): 9 | return 3.14159 * (self.radius**2) 10 | 11 | 12 | class Rectangle: 13 | def __init__(self, width, height): 14 | self.width = width 15 | self.height = height 16 | 17 | def area(self): 18 | return self.width * self.height 19 | 20 | 21 | def print_shape_info(shape): 22 | print(f"{shape.__class__.__name__} area: {shape.area()}") 23 | 24 | 25 | circle = Circle(5) 26 | rectangle = Rectangle(4, 6) 27 | 28 | print_shape_info(circle) # Circle area: 78.53975 29 | print_shape_info(rectangle) # Rectangle area: 24 30 | -------------------------------------------------------------------------------- /ch12/example.dynamically.typed.java.txt: -------------------------------------------------------------------------------- 1 | # example.dynamically.typed.java.txt 2 | String name = "John Doe"; 3 | int age = 60; 4 | -------------------------------------------------------------------------------- /ch12/example.strongly.typed.php: -------------------------------------------------------------------------------- 1 | # example.strongly.typed.php 2 | 7 | -------------------------------------------------------------------------------- /ch12/example.strongly.typed.py: -------------------------------------------------------------------------------- 1 | # example.strongly.typed.py 2 | a = 2 3 | b = "2" 4 | print(a + b) 5 | 6 | 7 | """ 8 | $ python ch12/example.strongly.typed.py 9 | Traceback (most recent call last): 10 | File "ch12/example.strongly.typed.py", line 3, in 11 | print(a + b) 12 | ~~^~~ 13 | TypeError: unsupported operand type(s) for +: 'int' and 'str' 14 | """ 15 | -------------------------------------------------------------------------------- /ch12/mypy_src/case.fixed.py: -------------------------------------------------------------------------------- 1 | # mypy_src/case.fixed.py 2 | from collections.abc import Iterable 3 | 4 | 5 | def title(names: Iterable[str | bytes]) -> list[str | bytes]: 6 | return [name.title() for name in names] 7 | 8 | 9 | print(title(["ALICE", "bob"])) # ['Alice', 'Bob'] - mypy OK 10 | print(title([b"ALICE", b"bob"])) # [b'Alice', b'Bob'] - mypy OK 11 | -------------------------------------------------------------------------------- /ch12/mypy_src/case.py: -------------------------------------------------------------------------------- 1 | # mypy_src/case.py 2 | from collections.abc import Iterable 3 | 4 | 5 | def title(names: Iterable[str]) -> list[str]: 6 | return [name.title() for name in names] 7 | 8 | 9 | print(title(["ALICE", "bob"])) # ['Alice', 'Bob'] - mypy OK 10 | print(title([b"ALICE", b"bob"])) # [b'Alice', b'Bob'] - mypy ERR 11 | -------------------------------------------------------------------------------- /ch12/mypy_src/simple_function.py: -------------------------------------------------------------------------------- 1 | # mypy_src/simple_function.py 2 | 3 | 4 | def hypothenuse(a, b): 5 | return (a**2 + b**2) ** 0.5 6 | -------------------------------------------------------------------------------- /ch12/mypy_src/simple_function_annotated.py: -------------------------------------------------------------------------------- 1 | # mypy_src/simple_function_annotated.py 2 | 3 | 4 | def hypothenuse(a: float, b: float) -> float: 5 | return (a**2 + b**2) ** 0.5 6 | 7 | 8 | print(hypothenuse(3, 4)) # This is fine 9 | print(hypothenuse(3.5, 4.9)) # This is also fine 10 | print(hypothenuse(complex(1, 2), 10)) # Type checker error 11 | -------------------------------------------------------------------------------- /ch12/requirements/requirements.in: -------------------------------------------------------------------------------- 1 | mypy 2 | -------------------------------------------------------------------------------- /ch12/requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | mypy==1.13.0 8 | # via -r requirements.in 9 | mypy-extensions==1.0.0 10 | # via mypy 11 | typing-extensions==4.12.2 12 | # via mypy 13 | -------------------------------------------------------------------------------- /ch13/.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | df.csv 3 | df.json 4 | df.xlsx 5 | -------------------------------------------------------------------------------- /ch13/ch13.jupyterlab-workspace: -------------------------------------------------------------------------------- 1 | {"data":{"layout-restorer:data":{"main":{"dock":{"type":"tab-area","currentIndex":0,"widgets":["notebook:ch13-dataprep.ipynb","notebook:ch13.ipynb"]},"current":"notebook:ch13-dataprep.ipynb"},"down":{"size":0,"widgets":[]},"left":{"collapsed":false,"current":"filebrowser","widgets":["filebrowser","running-sessions","@jupyterlab/toc:plugin","extensionmanager.main-view"]},"right":{"collapsed":true,"widgets":["jp-property-inspector","debugger-sidebar"]},"relativeSizes":[0.12670368500757193,0.8732963149924281,0]},"workspace-ui:lastSave":"ch13.jupyterlab-workspace","@jupyterlab/settingeditor-extension:plugin":{"sizes":[0.12337371018393899,0.876626289816061],"container":{"plugin":"@jupyterlab/apputils-extension:themes","sizes":[0.48540706605222733,0.5145929339477726]}},"notebook:ch13.ipynb":{"data":{"path":"ch13.ipynb","factory":"Notebook"}},"notebook:ch13-dataprep.ipynb":{"data":{"path":"ch13-dataprep.ipynb","factory":"Notebook"}}},"metadata":{"id":"ch13","last_modified":"2021-08-15T15:16:14.928473+00:00","created":"2021-08-15T15:16:14.928473+00:00"}} -------------------------------------------------------------------------------- /ch13/requirements/requirements.in: -------------------------------------------------------------------------------- 1 | arrow 2 | faker 3 | ipympl 4 | jupyterlab 5 | matplotlib 6 | notebook 7 | openpyxl 8 | pandas 9 | -------------------------------------------------------------------------------- /ch14/README.md: -------------------------------------------------------------------------------- 1 | # CH14 - Readme 2 | 3 | This chapter is about APIs. You will find two main folders: 4 | 5 | 1. `api_code` contains the FastAPI project about Railways 6 | 2. `samples`. This folder contains the examples that went in the chapter, so you can ignore it. 7 | 8 | ## Setup 9 | 10 | Make sure you make a copy of the `.env.example` file, called `.env` in the 11 | same folder. 12 | 13 | Install requirements with pip from their folder: 14 | 15 | $ pip install -r requirements.txt 16 | 17 | If you want to create your own dummy data, please also install the dev requirements: 18 | 19 | $ pip install -r dev.txt 20 | 21 | To generate a new database with random data, activate the virtual environment and then: 22 | 23 | $ cd api_code 24 | $ python dummy_data.py 25 | 26 | This will generate a new `train.db` file for you. 27 | 28 | ## Running the API 29 | 30 | Open a terminal window, change into the `api_code` folder and type this command: 31 | 32 | $ uvicorn main:app --reload 33 | 34 | The `--reload` flag makes sure uvicorn is reloaded if you make changes to the 35 | source while the app is running. 36 | 37 | ### Documentation 38 | 39 | To see the documentation and try out the endpoints, run the app and go to: 40 | 41 | http://localhost:8000/docs 42 | 43 | -------------------------------------------------------------------------------- /ch14/api_code/.env.example: -------------------------------------------------------------------------------- 1 | # api_code/.env 2 | SECRET_KEY="018ea65f62337ed59567a794b19dcaf8" 3 | DEBUG=true 4 | API_VERSION=2.0.0 5 | 6 | -------------------------------------------------------------------------------- /ch14/api_code/api/__init__.py: -------------------------------------------------------------------------------- 1 | # api_code/api/__init__.py 2 | -------------------------------------------------------------------------------- /ch14/api_code/api/admin.py: -------------------------------------------------------------------------------- 1 | # api_code/api/admin.py 2 | from typing import Optional 3 | 4 | from fastapi import ( 5 | APIRouter, 6 | Depends, 7 | Header, 8 | HTTPException, 9 | Response, 10 | status, 11 | ) 12 | from sqlalchemy.orm import Session 13 | 14 | from . import crud 15 | from .deps import Settings, get_db, get_settings 16 | from .util import is_admin 17 | 18 | 19 | router = APIRouter(prefix="/admin") 20 | 21 | 22 | def ensure_admin(settings: Settings, authorization: str): 23 | if not is_admin( 24 | settings=settings, authorization=authorization 25 | ): 26 | raise HTTPException( 27 | status_code=status.HTTP_403_FORBIDDEN, 28 | detail="You must be admin to access this endpoint.", 29 | ) 30 | 31 | 32 | @router.delete("/stations/{station_id}", tags=["Admin"]) 33 | def admin_delete_station( 34 | station_id: int, 35 | authorization: Optional[str] = Header(None), 36 | settings: Settings = Depends(get_settings), 37 | db: Session = Depends(get_db), 38 | ): 39 | ensure_admin(settings, authorization) 40 | row_count = crud.delete_station(db=db, station_id=station_id) 41 | if row_count: 42 | return Response(status_code=status.HTTP_204_NO_CONTENT) 43 | return Response(status_code=status.HTTP_404_NOT_FOUND) 44 | -------------------------------------------------------------------------------- /ch14/api_code/api/config.py: -------------------------------------------------------------------------------- 1 | # api_code/api/config.py 2 | from pydantic_settings import BaseSettings, SettingsConfigDict 3 | 4 | 5 | class Settings(BaseSettings): 6 | model_config = SettingsConfigDict(env_file=".env") 7 | 8 | secret_key: str 9 | debug: bool 10 | api_version: str 11 | -------------------------------------------------------------------------------- /ch14/api_code/api/database.py: -------------------------------------------------------------------------------- 1 | # api_code/api/database.py 2 | from sqlalchemy import create_engine, event 3 | from sqlalchemy.engine import Engine 4 | from sqlalchemy.orm import DeclarativeBase, sessionmaker 5 | 6 | from .config import Settings 7 | 8 | 9 | settings = Settings() 10 | 11 | 12 | DB_URL = "sqlite:///train.db" 13 | 14 | 15 | engine = create_engine( 16 | DB_URL, 17 | connect_args={"check_same_thread": False}, 18 | echo=settings.debug, # when debug is True, queries are logged 19 | ) 20 | 21 | 22 | @event.listens_for(Engine, "connect") 23 | def set_sqlite_pragma(dbapi_connection, connection_record): 24 | """Ensure foreign key constraints are enforced in SQLite. 25 | 26 | By default, SQLite ignores foreign key constraints. 27 | To enforce foreign key constraints, we need to set the 28 | foreign_keys pragma to ON. This needs to be done for each 29 | database connection. 30 | 31 | We use an event listener to execute the pragma statement every 32 | time SQLAlchemy establishes a connection to the database. 33 | 34 | See https://docs.sqlalchemy.org/en/20/dialects/sqlite.html 35 | """ 36 | cursor = dbapi_connection.cursor() 37 | cursor.execute("PRAGMA foreign_keys=ON") 38 | cursor.close() 39 | 40 | 41 | SessionLocal = sessionmaker( 42 | autocommit=False, 43 | autoflush=False, 44 | bind=engine, 45 | expire_on_commit=False, 46 | ) 47 | 48 | 49 | class Base(DeclarativeBase): 50 | pass 51 | -------------------------------------------------------------------------------- /ch14/api_code/api/deps.py: -------------------------------------------------------------------------------- 1 | # api_code/api/deps.py 2 | from functools import lru_cache 3 | 4 | from .config import Settings 5 | from .database import SessionLocal 6 | 7 | 8 | def get_db(): 9 | """Return a DB Session.""" 10 | with SessionLocal() as db: 11 | yield db 12 | 13 | 14 | @lru_cache 15 | def get_settings(): 16 | """Return the app settings.""" 17 | return Settings() 18 | -------------------------------------------------------------------------------- /ch14/api_code/api/tickets.py: -------------------------------------------------------------------------------- 1 | # api_code/api/tickets.py 2 | from fastapi import ( 3 | APIRouter, 4 | Depends, 5 | HTTPException, 6 | Response, 7 | status, 8 | ) 9 | from sqlalchemy.orm import Session 10 | 11 | from . import crud 12 | from .deps import get_db 13 | from .schemas import Ticket, TicketCreate 14 | 15 | 16 | router = APIRouter(prefix="/tickets") 17 | 18 | 19 | @router.get("", tags=["Tickets"]) 20 | def get_tickets(db: Session = Depends(get_db)) -> list[Ticket]: 21 | return crud.get_tickets(db=db) 22 | 23 | 24 | @router.get("/{ticket_id}", tags=["Tickets"]) 25 | def get_ticket( 26 | ticket_id: int, db: Session = Depends(get_db) 27 | ) -> Ticket: 28 | db_ticket = crud.get_ticket(db=db, ticket_id=ticket_id) 29 | if db_ticket is None: 30 | raise HTTPException( 31 | status_code=404, 32 | detail=f"Ticket {ticket_id} not found.", 33 | ) 34 | return db_ticket 35 | 36 | 37 | @router.post( 38 | "", 39 | status_code=status.HTTP_201_CREATED, 40 | tags=["Tickets"], 41 | ) 42 | def create_ticket( 43 | ticket: TicketCreate, db: Session = Depends(get_db) 44 | ) -> Ticket: 45 | return crud.create_ticket(db=db, ticket=ticket) 46 | 47 | 48 | @router.delete("/{ticket_id}", tags=["Tickets"]) 49 | def delete_ticket(ticket_id: int, db: Session = Depends(get_db)): 50 | row_count = crud.delete_ticket(db=db, ticket_id=ticket_id) 51 | if row_count: 52 | return Response(status_code=status.HTTP_204_NO_CONTENT) 53 | return Response(status_code=status.HTTP_404_NOT_FOUND) 54 | -------------------------------------------------------------------------------- /ch14/api_code/api/util.py: -------------------------------------------------------------------------------- 1 | # api_code/api/util.py 2 | from datetime import UTC, datetime, timedelta 3 | from typing import Optional 4 | 5 | import jwt 6 | from jwt.exceptions import PyJWTError 7 | 8 | from .deps import Settings 9 | 10 | 11 | ALGORITHM = "HS256" 12 | 13 | 14 | class InvalidToken(Exception): 15 | pass 16 | 17 | 18 | def create_token(payload: dict, key: str): 19 | now = datetime.now(UTC) 20 | data = { 21 | "iat": now, 22 | "exp": now + timedelta(hours=24), 23 | **payload, 24 | } 25 | return jwt.encode(data, key, algorithm=ALGORITHM) 26 | 27 | 28 | def extract_payload(token: str, key: str): 29 | try: 30 | return jwt.decode(token, key, algorithms=[ALGORITHM]) 31 | except PyJWTError as err: 32 | raise InvalidToken(str(err)) 33 | 34 | 35 | def is_admin( 36 | settings: Settings, authorization: Optional[str] = None 37 | ): 38 | if authorization is None: 39 | return False 40 | 41 | partition_key = ( 42 | "Bearer" if "Bearer" in authorization else "bearer" 43 | ) 44 | 45 | *dontcare, token = authorization.partition(partition_key) 46 | token = token.strip() 47 | 48 | try: 49 | payload = extract_payload(token, settings.secret_key) 50 | except InvalidToken: 51 | return False 52 | else: 53 | return payload.get("role") == "admin" 54 | -------------------------------------------------------------------------------- /ch14/api_code/main.py: -------------------------------------------------------------------------------- 1 | # api_code/main.py 2 | from api import admin, config, stations, tickets, trains, users 3 | from fastapi import FastAPI 4 | 5 | 6 | settings = config.Settings() 7 | 8 | app = FastAPI() 9 | 10 | app.include_router(admin.router) 11 | app.include_router(stations.router) 12 | app.include_router(trains.router) 13 | app.include_router(users.router) 14 | app.include_router(tickets.router) 15 | 16 | 17 | @app.get("/") 18 | def root(): 19 | return { 20 | "message": ( 21 | f"Welcome to version {settings.api_version} " 22 | f"of our API" 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /ch14/api_code/train.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Learn-Python-Programming-Fourth-Edition/980f6176943862553f2165f303789c77e0879eb9/ch14/api_code/train.db -------------------------------------------------------------------------------- /ch14/requirements/dev.in: -------------------------------------------------------------------------------- 1 | jupyterlab 2 | pdbpp 3 | faker 4 | isort 5 | black 6 | -------------------------------------------------------------------------------- /ch14/requirements/requirements.in: -------------------------------------------------------------------------------- 1 | fastapi 2 | pydantic[email] 3 | pydantic-settings 4 | pyjwt[crypto] 5 | python-dotenv 6 | sqlalchemy 7 | uvicorn 8 | -------------------------------------------------------------------------------- /ch14/requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | annotated-types==0.7.0 8 | # via pydantic 9 | anyio==4.6.2.post1 10 | # via starlette 11 | cffi==1.17.1 12 | # via cryptography 13 | click==8.1.7 14 | # via uvicorn 15 | cryptography==44.0.1 16 | # via pyjwt 17 | dnspython==2.7.0 18 | # via email-validator 19 | email-validator==2.2.0 20 | # via pydantic 21 | fastapi==0.115.5 22 | # via -r requirements.in 23 | greenlet==3.1.1 24 | # via sqlalchemy 25 | h11==0.14.0 26 | # via uvicorn 27 | idna==3.10 28 | # via 29 | # anyio 30 | # email-validator 31 | pycparser==2.22 32 | # via cffi 33 | pydantic[email]==2.10.1 34 | # via 35 | # -r requirements.in 36 | # fastapi 37 | # pydantic-settings 38 | pydantic-core==2.27.1 39 | # via pydantic 40 | pydantic-settings==2.6.1 41 | # via -r requirements.in 42 | pyjwt[crypto]==2.10.0 43 | # via -r requirements.in 44 | python-dotenv==1.0.1 45 | # via 46 | # -r requirements.in 47 | # pydantic-settings 48 | sniffio==1.3.1 49 | # via anyio 50 | sqlalchemy==2.0.36 51 | # via -r requirements.in 52 | starlette==0.41.3 53 | # via fastapi 54 | typing-extensions==4.12.2 55 | # via 56 | # fastapi 57 | # pydantic 58 | # pydantic-core 59 | # sqlalchemy 60 | uvicorn==0.32.1 61 | # via -r requirements.in 62 | -------------------------------------------------------------------------------- /ch14/samples/api.calls/users.txt: -------------------------------------------------------------------------------- 1 | $ http POST http://localhost:8000/users/authenticate \ 2 | email="fabrizio.romano@example.com" password="f4bPassword" 3 | HTTP/1.1 200 OK 4 | content-length: 201 5 | content-type: application/json 6 | date: Fri, 20 Aug 2021 21:51:13 GMT 7 | server: uvicorn 8 | 9 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MTIyMjQzNjEsImV4cCI6MTcxMjMxMDc2MSwiZW1haWwiOiJmYWJyaXppby5yb21hbm9AZXhhbXBsZS5jb20iLCJyb2xlIjoiYWRtaW4ifQ.4UiWgy5pqr85kR7ypB_L_01GK4QyzZje8NKMzBBVckc" 10 | 11 | --- 12 | 13 | -------------------------------------------------------------------------------- /ch15/argument_parsing/argv.py: -------------------------------------------------------------------------------- 1 | # argument_parsing/argv.py 2 | import sys 3 | 4 | 5 | print(sys.argv) 6 | -------------------------------------------------------------------------------- /ch15/project/.env.example: -------------------------------------------------------------------------------- 1 | url = 'http://localhost:8000/' 2 | secrets_dir = 'secrets/' 3 | -------------------------------------------------------------------------------- /ch15/project/railway_cli/__init__.py: -------------------------------------------------------------------------------- 1 | # project/railway_cli/__init__.py 2 | __version__ = "0.0.1" 3 | -------------------------------------------------------------------------------- /ch15/project/railway_cli/__main__.py: -------------------------------------------------------------------------------- 1 | # project/railway_cli/__main__.py 2 | from . import cli 3 | 4 | cli.main() 5 | -------------------------------------------------------------------------------- /ch15/project/railway_cli/api/__init__.py: -------------------------------------------------------------------------------- 1 | # project/railway_cli/api/__init__.py 2 | -------------------------------------------------------------------------------- /ch15/project/railway_cli/api/schemas.py: -------------------------------------------------------------------------------- 1 | # project/railway_cli/api/schemas.py 2 | """Pydantic schemas for objects received from the API.""" 3 | 4 | from datetime import datetime 5 | 6 | from pydantic import BaseModel, RootModel 7 | 8 | 9 | class Station(BaseModel): 10 | """A class to represent a station. 11 | 12 | This should match the API response schema for a station 13 | """ 14 | 15 | id: int 16 | code: str 17 | country: str 18 | city: str 19 | 20 | 21 | StationList = RootModel[list[Station]] 22 | 23 | 24 | class Train(BaseModel): 25 | """A class to represent a train. 26 | 27 | This should match the API response schema for a train 28 | """ 29 | 30 | id: int 31 | name: str 32 | station_from: Station 33 | station_to: Station 34 | departs_at: datetime 35 | arrives_at: datetime 36 | first_class: int 37 | second_class: int 38 | seats_per_car: int 39 | 40 | 41 | TrainList = RootModel[list[Train]] 42 | -------------------------------------------------------------------------------- /ch15/project/railway_cli/commands/__init__.py: -------------------------------------------------------------------------------- 1 | # project/railway_cli/commands/__init__.py 2 | import argparse 3 | 4 | from .admin import admin_commands 5 | from .base import Command 6 | from .stations import station_commands 7 | 8 | 9 | def configure_parsers(parser: argparse.ArgumentParser) -> None: 10 | """Configure the commandline argument parser. 11 | 12 | This adds a sub-parser for each command and configures it by 13 | calling the command's `configure_arg_parser` method. The 14 | command class is assigned to the `command` attribute of the 15 | parser. This allows the main function to access the command 16 | that was selected by the user. 17 | """ 18 | subparsers = parser.add_subparsers( 19 | description="Available commands", required=True 20 | ) 21 | 22 | command: type[Command] 23 | for command in [*admin_commands, *station_commands]: 24 | command_parser = subparsers.add_parser( 25 | command.name, help=command.help 26 | ) 27 | command.configure_arg_parser(command_parser) 28 | command_parser.set_defaults(command=command) 29 | -------------------------------------------------------------------------------- /ch15/project/railway_cli/commands/base.py: -------------------------------------------------------------------------------- 1 | # project/railway_cli/commands/base.py 2 | """This module defines a base class for commands.""" 3 | 4 | import argparse 5 | from typing import ClassVar 6 | 7 | from ..api.client import HTTPClient 8 | from ..config import get_settings 9 | 10 | 11 | class Command: 12 | """A base class to represent a command.""" 13 | 14 | name: ClassVar[str] 15 | help: ClassVar[str] 16 | 17 | def __init__(self, args: argparse.Namespace) -> None: 18 | self.args = args 19 | self.settings = get_settings(args) 20 | self.api_client = HTTPClient(self.settings.url) 21 | 22 | @classmethod 23 | def configure_arg_parser( 24 | cls, parser: argparse.ArgumentParser 25 | ) -> None: 26 | """Configure the parser for this command.""" 27 | raise NotImplementedError 28 | 29 | def execute(self) -> None: 30 | """Execute this command.""" 31 | raise NotImplementedError 32 | -------------------------------------------------------------------------------- /ch15/project/railway_cli/exceptions.py: -------------------------------------------------------------------------------- 1 | # project/railway_cli/exceptions.py 2 | """Exceptions for the railway CLI.""" 3 | 4 | 5 | class RailwayCLIError(Exception): 6 | """Base class for railway CLI exceptions.""" 7 | 8 | 9 | class APIError(RailwayCLIError): 10 | """An exception for errors coming from the API.""" 11 | 12 | 13 | class CommandError(RailwayCLIError): 14 | """An exception for errors in the CLI commands.""" 15 | 16 | 17 | class ConfigurationError(RailwayCLIError): 18 | """An exception for configuration errors.""" 19 | -------------------------------------------------------------------------------- /ch15/project/secrets/railway_api_email: -------------------------------------------------------------------------------- 1 | fabrizio.romano@example.com 2 | -------------------------------------------------------------------------------- /ch15/project/secrets/railway_api_password: -------------------------------------------------------------------------------- 1 | f4bPassword 2 | -------------------------------------------------------------------------------- /ch15/requirements/dev.in: -------------------------------------------------------------------------------- 1 | -c requirements.txt 2 | black 3 | isort 4 | flake8 5 | mypy 6 | types-requests 7 | -------------------------------------------------------------------------------- /ch15/requirements/dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile dev.in 6 | # 7 | black==24.10.0 8 | # via -r dev.in 9 | click==8.1.7 10 | # via black 11 | flake8==7.1.1 12 | # via -r dev.in 13 | isort==5.13.2 14 | # via -r dev.in 15 | mccabe==0.7.0 16 | # via flake8 17 | mypy==1.13.0 18 | # via -r dev.in 19 | mypy-extensions==1.0.0 20 | # via 21 | # black 22 | # mypy 23 | packaging==24.2 24 | # via black 25 | pathspec==0.12.1 26 | # via black 27 | platformdirs==4.3.6 28 | # via black 29 | pycodestyle==2.12.1 30 | # via flake8 31 | pyflakes==3.2.0 32 | # via flake8 33 | types-requests==2.32.0.20241016 34 | # via -r dev.in 35 | typing-extensions==4.12.2 36 | # via 37 | # -c /Users/fab/code/lpp4ed/ch15/requirements/requirements.txt 38 | # mypy 39 | urllib3==2.2.3 40 | # via 41 | # -c /Users/fab/code/lpp4ed/ch15/requirements/requirements.txt 42 | # types-requests 43 | -------------------------------------------------------------------------------- /ch15/requirements/requirements.in: -------------------------------------------------------------------------------- 1 | pydantic[email] 2 | pydantic-settings 3 | requests 4 | -------------------------------------------------------------------------------- /ch15/requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | annotated-types==0.7.0 8 | # via pydantic 9 | certifi==2024.8.30 10 | # via requests 11 | charset-normalizer==3.4.0 12 | # via requests 13 | dnspython==2.7.0 14 | # via email-validator 15 | email-validator==2.2.0 16 | # via pydantic 17 | idna==3.10 18 | # via 19 | # email-validator 20 | # requests 21 | pydantic[email]==2.10.1 22 | # via 23 | # -r requirements.in 24 | # pydantic-settings 25 | pydantic-core==2.27.1 26 | # via pydantic 27 | pydantic-settings==2.6.1 28 | # via -r requirements.in 29 | python-dotenv==1.0.1 30 | # via pydantic-settings 31 | requests==2.32.3 32 | # via -r requirements.in 33 | typing-extensions==4.12.2 34 | # via 35 | # pydantic 36 | # pydantic-core 37 | urllib3==2.2.3 38 | # via requests 39 | -------------------------------------------------------------------------------- /ch16/pypirc: -------------------------------------------------------------------------------- 1 | # This file contains an example of the format of your ~/.pypirc 2 | # file 3 | 4 | [testpypi] 5 | username = __token__ 6 | password = pypi-... 7 | 8 | [pypi] 9 | username = __token__ 10 | password = pypi-... 11 | -------------------------------------------------------------------------------- /ch16/railway-project/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 66 3 | -------------------------------------------------------------------------------- /ch16/railway-project/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## Version 0.0.1 4 | 5 | Initial published version. 6 | -------------------------------------------------------------------------------- /ch16/railway-project/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Heinrich Kruger, Fabrizio Romano 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /ch16/railway-project/README.md: -------------------------------------------------------------------------------- 1 | # Railway CLI 2 | 3 | A railway CLI application to demonstrate packaging and distribution of a Python project for 4 | Chapter 16 of "Learn Python Programming, 4d Edition", by Fabrizio Romano and Heinrich Kruger. At the 5 | same time, it acts as an example of an application built around the trains API project from Chapter 6 | 14 of the same book. 7 | 8 | ## Usage 9 | 10 | The application provides a command-line interface for interacting with the railway API. 11 | 12 | To get help on the available commands, run: 13 | 14 | $ railway-cli -h 15 | 16 | To get help for a specific command, run: 17 | 18 | $ python -m railway_cli -h 19 | 20 | For example, to get help for the `get-station` command, run: 21 | 22 | $ python -m railway_cli get-station -h 23 | -------------------------------------------------------------------------------- /ch16/railway-project/src/railway_cli/__init__.py: -------------------------------------------------------------------------------- 1 | # railway-project/src/railway_cli/__init__.py 2 | __version__ = "0.0.1" 3 | -------------------------------------------------------------------------------- /ch16/railway-project/src/railway_cli/__main__.py: -------------------------------------------------------------------------------- 1 | # railway-project/src/railway_cli/__main__.py 2 | import sys 3 | from . import cli 4 | 5 | sys.argv[0] = f"python -m {__package__}" 6 | cli.main() 7 | -------------------------------------------------------------------------------- /ch16/railway-project/src/railway_cli/api/__init__.py: -------------------------------------------------------------------------------- 1 | # railway-project/src/railway_cli/api/__init__.py 2 | -------------------------------------------------------------------------------- /ch16/railway-project/src/railway_cli/api/schemas.py: -------------------------------------------------------------------------------- 1 | # railway-project/src/railway_cli/api/schemas.py 2 | """Pydantic schemas for objects received from the API.""" 3 | 4 | from datetime import datetime 5 | 6 | from pydantic import BaseModel, RootModel 7 | 8 | 9 | class Station(BaseModel): 10 | """A class to represent a station. 11 | 12 | This should match the API response schema for a station 13 | """ 14 | 15 | id: int 16 | code: str 17 | country: str 18 | city: str 19 | 20 | 21 | StationList = RootModel[list[Station]] 22 | 23 | 24 | class Train(BaseModel): 25 | """A class to represent a train. 26 | 27 | This should match the API response schema for a train 28 | """ 29 | 30 | id: int 31 | name: str 32 | station_from: Station 33 | station_to: Station 34 | departs_at: datetime 35 | arrives_at: datetime 36 | first_class: int 37 | second_class: int 38 | seats_per_car: int 39 | 40 | 41 | TrainList = RootModel[list[Train]] 42 | -------------------------------------------------------------------------------- /ch16/railway-project/src/railway_cli/commands/__init__.py: -------------------------------------------------------------------------------- 1 | # railway-project/src/railway_cli/commands/__init__.py 2 | import argparse 3 | 4 | from .admin import admin_commands 5 | from .base import Command 6 | from .stations import station_commands 7 | 8 | 9 | def configure_parsers(parser: argparse.ArgumentParser) -> None: 10 | """Configure the commandline argument parser. 11 | 12 | This adds a sub-parser for each command and configures it by 13 | calling the command's `configure_arg_parser` method. The 14 | command class is assigned to the `command` attribute of the 15 | parser. This allows the main function to access the command 16 | that was selected by the user. 17 | """ 18 | subparsers = parser.add_subparsers( 19 | description="Available commands", required=True 20 | ) 21 | 22 | command: type[Command] 23 | for command in [*admin_commands, *station_commands]: 24 | command_parser = subparsers.add_parser( 25 | command.name, help=command.help 26 | ) 27 | command.configure_arg_parser(command_parser) 28 | command_parser.set_defaults(command=command) 29 | -------------------------------------------------------------------------------- /ch16/railway-project/src/railway_cli/commands/base.py: -------------------------------------------------------------------------------- 1 | # railway-project/src/railway_cli/commands/base.py 2 | """This module defines a base class for commands.""" 3 | 4 | import argparse 5 | from typing import ClassVar 6 | 7 | from ..api.client import HTTPClient 8 | from ..config import get_settings 9 | 10 | 11 | class Command: 12 | """A base class to represent a command.""" 13 | 14 | name: ClassVar[str] 15 | help: ClassVar[str] 16 | 17 | def __init__(self, args: argparse.Namespace) -> None: 18 | self.args = args 19 | self.settings = get_settings(args) 20 | self.api_client = HTTPClient(self.settings.url) 21 | 22 | @classmethod 23 | def configure_arg_parser( 24 | cls, parser: argparse.ArgumentParser 25 | ) -> None: 26 | """Configure the parser for this command.""" 27 | raise NotImplementedError 28 | 29 | def execute(self) -> None: 30 | """Execute this command.""" 31 | raise NotImplementedError 32 | -------------------------------------------------------------------------------- /ch16/railway-project/src/railway_cli/exceptions.py: -------------------------------------------------------------------------------- 1 | # railway-project/src/railway_cli/exceptions.py 2 | """Exceptions for the railway CLI.""" 3 | 4 | 5 | class RailwayCLIError(Exception): 6 | """Base class for railway CLI exceptions.""" 7 | 8 | 9 | class APIError(RailwayCLIError): 10 | """An exception for errors coming from the API.""" 11 | 12 | 13 | class CommandError(RailwayCLIError): 14 | """An exception for errors in the CLI commands.""" 15 | 16 | 17 | class ConfigurationError(RailwayCLIError): 18 | """An exception for configuration errors.""" 19 | -------------------------------------------------------------------------------- /ch16/railway-project/test/__init__.py: -------------------------------------------------------------------------------- 1 | # railway-project/test/__init__.py 2 | -------------------------------------------------------------------------------- /ch16/requirements/build.in: -------------------------------------------------------------------------------- 1 | build 2 | twine 3 | -------------------------------------------------------------------------------- /ch16/requirements/build.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile build.in 6 | # 7 | build==1.2.2.post1 8 | # via -r build.in 9 | certifi==2024.8.30 10 | # via requests 11 | charset-normalizer==3.4.0 12 | # via requests 13 | docutils==0.21.2 14 | # via readme-renderer 15 | idna==3.10 16 | # via requests 17 | importlib-metadata==8.5.0 18 | # via twine 19 | jaraco-classes==3.4.0 20 | # via keyring 21 | jaraco-context==6.0.1 22 | # via keyring 23 | jaraco-functools==4.1.0 24 | # via keyring 25 | keyring==25.5.0 26 | # via twine 27 | markdown-it-py==3.0.0 28 | # via rich 29 | mdurl==0.1.2 30 | # via markdown-it-py 31 | more-itertools==10.5.0 32 | # via 33 | # jaraco-classes 34 | # jaraco-functools 35 | nh3==0.2.18 36 | # via readme-renderer 37 | packaging==24.2 38 | # via build 39 | pkginfo==1.10.0 40 | # via twine 41 | pygments==2.18.0 42 | # via 43 | # readme-renderer 44 | # rich 45 | pyproject-hooks==1.2.0 46 | # via build 47 | readme-renderer==44.0 48 | # via twine 49 | requests==2.32.3 50 | # via 51 | # requests-toolbelt 52 | # twine 53 | requests-toolbelt==1.0.0 54 | # via twine 55 | rfc3986==2.0.0 56 | # via twine 57 | rich==13.9.4 58 | # via twine 59 | twine==5.1.1 60 | # via -r build.in 61 | urllib3==2.2.3 62 | # via 63 | # requests 64 | # twine 65 | zipp==3.21.0 66 | # via importlib-metadata 67 | -------------------------------------------------------------------------------- /ch16/requirements/dev.in: -------------------------------------------------------------------------------- 1 | -c requirements.txt 2 | 3 | black 4 | flake8 5 | isort 6 | mypy 7 | pytest 8 | pytest-mock 9 | requests-mock 10 | types-requests 11 | -------------------------------------------------------------------------------- /ch16/requirements/requirements.in: -------------------------------------------------------------------------------- 1 | pydantic[email] 2 | pydantic-settings 3 | requests 4 | -------------------------------------------------------------------------------- /ch16/requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | annotated-types==0.7.0 8 | # via pydantic 9 | certifi==2024.8.30 10 | # via requests 11 | charset-normalizer==3.4.0 12 | # via requests 13 | dnspython==2.7.0 14 | # via email-validator 15 | email-validator==2.2.0 16 | # via pydantic 17 | idna==3.10 18 | # via 19 | # email-validator 20 | # requests 21 | pydantic[email]==2.10.1 22 | # via 23 | # -r requirements.in 24 | # pydantic-settings 25 | pydantic-core==2.27.1 26 | # via pydantic 27 | pydantic-settings==2.6.1 28 | # via -r requirements.in 29 | python-dotenv==1.0.1 30 | # via pydantic-settings 31 | requests==2.32.3 32 | # via -r requirements.in 33 | typing-extensions==4.12.2 34 | # via 35 | # pydantic 36 | # pydantic-core 37 | urllib3==2.2.3 38 | # via requests 39 | -------------------------------------------------------------------------------- /ch16/skeleton-project/README.md: -------------------------------------------------------------------------------- 1 | # Simple skeleton project 2 | 3 | This is a skeleton of a project that you can copy and flesh out to create your own project. 4 | -------------------------------------------------------------------------------- /ch16/skeleton-project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "your-projec-name" 7 | authors = [ 8 | {name="Your Name", email="your.email@example.com"}, 9 | ] 10 | version = "0.0.0" 11 | description = "A short description of your project" 12 | readme = "README.md" 13 | 14 | [project.urls] 15 | Homepage = "https://example.com/your/project" 16 | 17 | [project.optional-dependencies] 18 | test = [ 19 | "pytest" 20 | ] 21 | -------------------------------------------------------------------------------- /ch16/skeleton-project/src/example/__init__.py: -------------------------------------------------------------------------------- 1 | # skeleton-project/example/__init__.py 2 | """This is just a placeholder package, replace it with your own 3 | package""" 4 | 5 | 6 | print("Hello world") 7 | -------------------------------------------------------------------------------- /ch16/skeleton-project/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # skeleton-project/tests/__init__.py 2 | -------------------------------------------------------------------------------- /ch17/input11.txt: -------------------------------------------------------------------------------- 1 | ...#...... 2 | .......#.. 3 | #......... 4 | .......... 5 | ......#... 6 | .#........ 7 | .........# 8 | .......... 9 | .......#.. 10 | #...#..... 11 | -------------------------------------------------------------------------------- /ch17/input7.txt: -------------------------------------------------------------------------------- 1 | 32T3K 765 2 | T55J5 684 3 | KK677 28 4 | KTJJT 220 5 | QQQJA 483 6 | -------------------------------------------------------------------------------- /ch17/requirements/requirements.in: -------------------------------------------------------------------------------- 1 | mypy 2 | -------------------------------------------------------------------------------- /ch17/requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | mypy==1.13.0 8 | # via -r requirements.in 9 | mypy-extensions==1.0.0 10 | # via mypy 11 | typing-extensions==4.12.2 12 | # via mypy 13 | -------------------------------------------------------------------------------- /ch17/util.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | 4 | def get_input(fname: str, func: Callable | None = None): 5 | """Return inputs. 6 | 7 | Input is optionally parsed by `func`. When it's the case of 8 | empty lines, `func` isn't applied, and an empty string is 9 | returned. 10 | """ 11 | if func is None: 12 | func = lambda x: x 13 | with open(fname) as s: 14 | return [ 15 | func(stripped) if (stripped := l.rstrip()) else "" 16 | for l in s 17 | ] 18 | --------------------------------------------------------------------------------