├── labs ├── lab04 │ ├── task_1 │ │ ├── list2.txt │ │ ├── list3.txt │ │ ├── list5.txt │ │ ├── list10.txt │ │ ├── list4.txt │ │ ├── list6.txt │ │ ├── list7.txt │ │ ├── list8.txt │ │ ├── list9.txt │ │ ├── list11.txt │ │ └── list1.txt │ └── lab04.ipynb ├── lab06 │ ├── solution-example │ │ ├── app │ │ │ ├── __init__.py │ │ │ ├── exceptions │ │ │ │ ├── __init__.py │ │ │ │ └── validation.py │ │ │ ├── models │ │ │ │ ├── note.py │ │ │ │ └── __init__.py │ │ │ ├── ui │ │ │ │ ├── success_messages.py │ │ │ │ ├── note_details.py │ │ │ │ ├── __init__.py │ │ │ │ └── notes_list.py │ │ │ ├── storage │ │ │ │ ├── __init__.py │ │ │ │ └── json_file.py │ │ │ └── actions │ │ │ │ ├── __init__.py │ │ │ │ ├── delete.py │ │ │ │ ├── add.py │ │ │ │ └── edit.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── ui │ │ │ │ ├── __init__.py │ │ │ │ ├── test_note_details.py │ │ │ │ └── test_notes_list.py │ │ │ └── actions │ │ │ │ ├── __init__.py │ │ │ │ ├── test_delete.py │ │ │ │ ├── test_edit.py │ │ │ │ └── test_add.py │ │ ├── .gitignore │ │ ├── main.py │ │ └── README.md │ ├── spaghetti-notes │ │ ├── actions │ │ │ ├── __init__.py │ │ │ ├── list.py │ │ │ ├── delete.py │ │ │ ├── view.py │ │ │ └── add.py │ │ ├── .gitignore │ │ └── main.py │ └── README.md ├── lab03 │ └── lab03.md ├── lab05 │ └── lab05.md └── lab02 │ └── lab02.ipynb ├── 14 - Testing ├── tests │ ├── __init__.py │ ├── mocks │ │ ├── __init__.py │ │ └── mock_player.py │ ├── test_ai_player.py │ └── test_engine.py ├── test_single_simple_pytest.py ├── assets │ ├── tests.png │ ├── pycharm.png │ ├── vscode-bottom.png │ ├── vscode-python.png │ └── vscode-testui.png ├── test_single_simple_unittest.py └── game │ ├── level.py │ ├── players │ ├── input_player.py │ └── ai.py │ ├── player.py │ └── engine.py ├── project-setup-demo ├── .gitignore ├── requirements.txt ├── configs │ ├── ai-easy.yaml │ └── input-easy copy.yaml ├── requirements-dev.txt ├── game │ ├── level.py │ ├── players │ │ ├── input_player.py │ │ ├── mock_player.py │ │ └── ai.py │ ├── player.py │ └── engine.py ├── config_parser │ └── parser.py ├── README.md └── main.py ├── 12 - Modules ├── script.py ├── hitchhikers.py ├── game │ ├── level.py │ ├── players │ │ ├── input_player.py │ │ ├── mock_player.py │ │ └── ai.py │ ├── player.py │ └── engine.py └── besenitsa.py ├── 15 - Web programming └── examples │ ├── dev.conf │ ├── static │ ├── logo.png │ └── style.css │ ├── simple_app.py │ ├── templates │ ├── index.html │ ├── user.html │ ├── custom_404.html │ ├── fancy_index.html │ ├── base.html │ └── login.html │ ├── api_jsonify.py │ ├── fancy_page.py │ ├── simple_templates.py │ ├── simple_login.py │ ├── accessing_headers.py │ ├── make_response_example.py │ ├── config_example.py │ ├── external_config_example.py │ ├── custom_404.py │ ├── simple_user_page.py │ ├── login_page.py │ └── protected_login_page.py ├── misc ├── logo.png └── jupyter-edit.png ├── 11 - Git ├── local-changes.png └── README.md ├── 09 - Multithreading └── assets │ ├── t1.jpg │ └── t2.jpg ├── 01 - Intro to Python ├── assets │ ├── github.png │ ├── idle.png │ ├── vscode.png │ ├── jupyter.png │ ├── pythons.png │ ├── pythoncmd.png │ ├── collab-light.png │ ├── new-local-clone.png │ └── vscode-jupyter.png ├── notebooks.md └── install-n-setup.md ├── 06 - Typing Hints └── assets │ ├── mypy-vscode.png │ ├── mypy-in-action.png │ └── pylance-setting.png ├── 16 - Using C code in Python ├── C │ ├── simple_function │ │ ├── sum.c │ │ ├── sum.h │ │ └── CMakeLists.txt │ ├── structs │ │ ├── CMakeLists.txt │ │ ├── rational.h │ │ └── rational.c │ └── arrays_pointers │ │ ├── CMakeLists.txt │ │ ├── dynamic_array.h │ │ └── dynamic_array.c └── C++ │ ├── CMakeLists.txt │ ├── dynamic_array.h │ └── dynamic_array.cpp ├── 00 - Course intro └── 00 - Програмиране с Python.pdf ├── 07 - Exceptions Handling └── assets │ └── try_except_else_finally.png ├── price-tracker-demo ├── requirements.txt ├── scraper.py ├── cli.py ├── state.py ├── state.json ├── main.py └── item.py ├── pywc ├── sample.txt └── wc.py ├── 08 - Files └── files │ └── 1.txt ├── _config.yml ├── 13 - Clean code ├── example1.py ├── example1_refactored.py └── example2.py ├── .github └── workflows │ └── renderbook.yml ├── example_projects.md ├── 03 - OOP ├── example1.py └── example2.py ├── _toc.yml ├── .gitignore ├── console-demo └── app.py ├── 04 - Functional Programming └── tasks │ └── tasks.md ├── projects.md └── README.md /labs/lab04/task_1/list2.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /14 - Testing/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /14 - Testing/tests/mocks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /project-setup-demo/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/tests/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /labs/lab06/spaghetti-notes/actions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/tests/actions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /project-setup-demo/requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML==6.0 2 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/app/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /labs/lab04/task_1/list3.txt: -------------------------------------------------------------------------------- 1 | item1:1:1.75 2 | item2:5:2.03 3 | item3:7:3.33 -------------------------------------------------------------------------------- /labs/lab04/task_1/list5.txt: -------------------------------------------------------------------------------- 1 | -:1:1.75 2 | -item2:5:2.03 3 | -item3:7:3.33 -------------------------------------------------------------------------------- /labs/lab06/solution-example/.gitignore: -------------------------------------------------------------------------------- 1 | notes.json 2 | __pycache__/ 3 | -------------------------------------------------------------------------------- /labs/lab06/spaghetti-notes/.gitignore: -------------------------------------------------------------------------------- 1 | notes.json 2 | __pycache__/ 3 | -------------------------------------------------------------------------------- /12 - Modules/script.py: -------------------------------------------------------------------------------- 1 | import hitchhikers 2 | 3 | print(hitchhikers.compute()) -------------------------------------------------------------------------------- /14 - Testing/test_single_simple_pytest.py: -------------------------------------------------------------------------------- 1 | def test_one(): 2 | assert 1 == 1 -------------------------------------------------------------------------------- /15 - Web programming/examples/dev.conf: -------------------------------------------------------------------------------- 1 | TESTING = True 2 | ENV = 'development' -------------------------------------------------------------------------------- /labs/lab04/task_1/list10.txt: -------------------------------------------------------------------------------- 1 | -item1:1:1.75 2 | -item2:1:-3.14 3 | -item3:3:3.33 -------------------------------------------------------------------------------- /labs/lab04/task_1/list4.txt: -------------------------------------------------------------------------------- 1 | -item1:1:1.75 2 | -item2,5:2.03 3 | -item3:7:3.33 -------------------------------------------------------------------------------- /labs/lab04/task_1/list6.txt: -------------------------------------------------------------------------------- 1 | -item1:1:1.75 2 | -item2:5:2.03 3 | -item3:asd:3.33 -------------------------------------------------------------------------------- /labs/lab04/task_1/list7.txt: -------------------------------------------------------------------------------- 1 | -item1:1:1.75 2 | -item2:5.2:2.03 3 | -item3:3:3.33 -------------------------------------------------------------------------------- /labs/lab04/task_1/list8.txt: -------------------------------------------------------------------------------- 1 | -item1:1:1.75 2 | -item2:-1:2.03 3 | -item3:3:3.33 -------------------------------------------------------------------------------- /labs/lab04/task_1/list9.txt: -------------------------------------------------------------------------------- 1 | -item1:1:1.75 2 | -item2:1:асд 3 | -item3:3:3.33 -------------------------------------------------------------------------------- /labs/lab04/task_1/list11.txt: -------------------------------------------------------------------------------- 1 | -item1:1:1.75 2 | -item2:1:3.14 3 | -item3:3:3.33:3.1 -------------------------------------------------------------------------------- /misc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/misc/logo.png -------------------------------------------------------------------------------- /misc/jupyter-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/misc/jupyter-edit.png -------------------------------------------------------------------------------- /11 - Git/local-changes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/11 - Git/local-changes.png -------------------------------------------------------------------------------- /labs/lab04/task_1/list1.txt: -------------------------------------------------------------------------------- 1 | - мляко:2:2.50 2 | - хляб:1:1.50 3 | - банани:1:2.50 4 | - ябълки:1:0.50 5 | - круши:1:1.75 -------------------------------------------------------------------------------- /project-setup-demo/configs/ai-easy.yaml: -------------------------------------------------------------------------------- 1 | ai_player: true 2 | 3 | level: 4 | word: SCRIPT 5 | failed_attempts: 10 -------------------------------------------------------------------------------- /14 - Testing/assets/tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/14 - Testing/assets/tests.png -------------------------------------------------------------------------------- /09 - Multithreading/assets/t1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/09 - Multithreading/assets/t1.jpg -------------------------------------------------------------------------------- /09 - Multithreading/assets/t2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/09 - Multithreading/assets/t2.jpg -------------------------------------------------------------------------------- /14 - Testing/assets/pycharm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/14 - Testing/assets/pycharm.png -------------------------------------------------------------------------------- /project-setup-demo/configs/input-easy copy.yaml: -------------------------------------------------------------------------------- 1 | ai_player: false 2 | 3 | level: 4 | word: SCRIPTING 5 | failed_attempts: 8 -------------------------------------------------------------------------------- /01 - Intro to Python/assets/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/01 - Intro to Python/assets/github.png -------------------------------------------------------------------------------- /01 - Intro to Python/assets/idle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/01 - Intro to Python/assets/idle.png -------------------------------------------------------------------------------- /01 - Intro to Python/assets/vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/01 - Intro to Python/assets/vscode.png -------------------------------------------------------------------------------- /14 - Testing/assets/vscode-bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/14 - Testing/assets/vscode-bottom.png -------------------------------------------------------------------------------- /14 - Testing/assets/vscode-python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/14 - Testing/assets/vscode-python.png -------------------------------------------------------------------------------- /14 - Testing/assets/vscode-testui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/14 - Testing/assets/vscode-testui.png -------------------------------------------------------------------------------- /01 - Intro to Python/assets/jupyter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/01 - Intro to Python/assets/jupyter.png -------------------------------------------------------------------------------- /01 - Intro to Python/assets/pythons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/01 - Intro to Python/assets/pythons.png -------------------------------------------------------------------------------- /06 - Typing Hints/assets/mypy-vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/06 - Typing Hints/assets/mypy-vscode.png -------------------------------------------------------------------------------- /16 - Using C code in Python/C/simple_function/sum.c: -------------------------------------------------------------------------------- 1 | #include "sum.h" 2 | 3 | int sum(const int a, const int b) { 4 | return a + b; 5 | } -------------------------------------------------------------------------------- /01 - Intro to Python/assets/pythoncmd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/01 - Intro to Python/assets/pythoncmd.png -------------------------------------------------------------------------------- /06 - Typing Hints/assets/mypy-in-action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/06 - Typing Hints/assets/mypy-in-action.png -------------------------------------------------------------------------------- /01 - Intro to Python/assets/collab-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/01 - Intro to Python/assets/collab-light.png -------------------------------------------------------------------------------- /06 - Typing Hints/assets/pylance-setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/06 - Typing Hints/assets/pylance-setting.png -------------------------------------------------------------------------------- /15 - Web programming/examples/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/15 - Web programming/examples/static/logo.png -------------------------------------------------------------------------------- /project-setup-demo/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | mypy==0.991 3 | mypy-extensions==0.4.3 4 | tomli==2.0.1 5 | typing_extensions==4.4.0 6 | -------------------------------------------------------------------------------- /00 - Course intro/00 - Програмиране с Python.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/00 - Course intro/00 - Програмиране с Python.pdf -------------------------------------------------------------------------------- /01 - Intro to Python/assets/new-local-clone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/01 - Intro to Python/assets/new-local-clone.png -------------------------------------------------------------------------------- /01 - Intro to Python/assets/vscode-jupyter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/01 - Intro to Python/assets/vscode-jupyter.png -------------------------------------------------------------------------------- /project-setup-demo/game/level.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | @dataclass(frozen=True) 4 | class Level: 5 | word: str 6 | failed_attempts: int -------------------------------------------------------------------------------- /16 - Using C code in Python/C/simple_function/sum.h: -------------------------------------------------------------------------------- 1 | #ifndef EXAMPLE_1_SUM_H 2 | #define EXAMPLE_1_SUM_H 3 | 4 | extern int sum(const int a, const int b); 5 | 6 | #endif -------------------------------------------------------------------------------- /07 - Exceptions Handling/assets/try_except_else_finally.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmipython/PythonCourse2024/HEAD/07 - Exceptions Handling/assets/try_except_else_finally.png -------------------------------------------------------------------------------- /15 - Web programming/examples/simple_app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/") 6 | def hello_world(): 7 | return "

Hello, World!

" -------------------------------------------------------------------------------- /15 - Web programming/examples/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

This is the home page

5 |

Hi, {{user}}

6 | {% endblock %} -------------------------------------------------------------------------------- /15 - Web programming/examples/templates/user.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

This is the user page

5 |

Hi, {{user}}

6 | {% endblock %} -------------------------------------------------------------------------------- /price-tracker-demo/requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.12.2 2 | certifi==2023.11.17 3 | charset-normalizer==3.3.2 4 | idna==3.6 5 | requests==2.31.0 6 | soupsieve==2.5 7 | urllib3==2.1.0 8 | -------------------------------------------------------------------------------- /16 - Using C code in Python/C/structs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | project(Structs C) 3 | 4 | set(CMAKE_C_STANDARD 99) 5 | 6 | add_library(Structs SHARED rational.c rational.h) -------------------------------------------------------------------------------- /15 - Web programming/examples/api_jsonify.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/") 6 | def home(): 7 | return jsonify(status='success', message='Hello, World!') -------------------------------------------------------------------------------- /16 - Using C code in Python/C/simple_function/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | project(SimpleFunction C) 3 | 4 | set(CMAKE_C_STANDARD 99) 5 | 6 | add_library(SimpleFunction SHARED sum.c sum.h) -------------------------------------------------------------------------------- /16 - Using C code in Python/C++/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | project(DynamicArray) 3 | 4 | set(CMAKE_CXX_STANDARD 11) 5 | 6 | add_library(DynamicArray SHARED dynamic_array.cpp dynamic_array.h) -------------------------------------------------------------------------------- /14 - Testing/test_single_simple_unittest.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | class TestClass(unittest.TestCase): 4 | def test_one(self): 5 | self.assertEqual(1, 1) 6 | 7 | if __name__ == '__main__': 8 | unittest.main() -------------------------------------------------------------------------------- /16 - Using C code in Python/C/arrays_pointers/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | project(SimpleFunction C) 3 | 4 | set(CMAKE_C_STANDARD 99) 5 | 6 | add_library(DynamicArray SHARED dynamic_array.c dynamic_array.h) -------------------------------------------------------------------------------- /15 - Web programming/examples/fancy_page.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/") 6 | def home(): 7 | return render_template('fancy_index.html', title='Home', user='Lyubo') 8 | -------------------------------------------------------------------------------- /15 - Web programming/examples/simple_templates.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/") 6 | def home(): 7 | return render_template('index.html', title='Home', user='Lyubo') 8 | -------------------------------------------------------------------------------- /15 - Web programming/examples/simple_login.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/") 6 | def home(): 7 | return render_template('index.html', title='Home', user='Lyubo') 8 | 9 | @app.route("/login") 10 | def login(): 11 | return render_template('login.html', title='Login') -------------------------------------------------------------------------------- /12 - Modules/hitchhikers.py: -------------------------------------------------------------------------------- 1 | ANSWER = 42 2 | 3 | def compute(): 4 | from time import sleep 5 | print("Hm, I'll have to think about that. Return to this place in exactly 7.5 million years...") 6 | sleep(0.75) 7 | return ANSWER 8 | 9 | class TheGreatDeepThought: 10 | def ask(self): 11 | print("Shush! The show is back on.") -------------------------------------------------------------------------------- /15 - Web programming/examples/accessing_headers.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, make_response 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/") 6 | def home(): 7 | if 'user' in request.headers: 8 | user = request.headers['user'] 9 | return make_response(f"Hi, {user}", 200) 10 | return make_response("Hi, Guest", 200) -------------------------------------------------------------------------------- /15 - Web programming/examples/make_response_example.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, make_response 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/") 6 | def home(): 7 | return make_response("Hello World!", 200, {"Debug": "Hello World!"}) 8 | 9 | @app.route("/error") 10 | def error(): 11 | return make_response("Error!", 404, {"Debug": "Error!"}) -------------------------------------------------------------------------------- /15 - Web programming/examples/templates/custom_404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block styles %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

Page was not found

9 | 10 | {% endblock %} -------------------------------------------------------------------------------- /14 - Testing/tests/test_ai_player.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from game.players.ai import AI 4 | 5 | BIG_HP = 42 6 | 7 | class TestAIPlayer(unittest.TestCase): 8 | def test_firstGuess_isE(self): 9 | ai = AI(BIG_HP) 10 | 11 | guess = ai.guess("S_T", set()) 12 | 13 | self.assertEqual(guess, "E") 14 | 15 | if __name__ == '__main__': 16 | unittest.main() -------------------------------------------------------------------------------- /15 - Web programming/examples/static/style.css: -------------------------------------------------------------------------------- 1 | #main-content { 2 | width: 100%; 3 | height: 100vh; 4 | background-color: #0D1117; 5 | padding: 10px; 6 | border: 1px solid #ccc; 7 | border-radius: 5px; 8 | box-shadow: 0 0 10px #ccc; 9 | } 10 | 11 | h1, h2 { 12 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 13 | color: white; 14 | } -------------------------------------------------------------------------------- /12 - Modules/game/level.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | @dataclass(frozen=True) 4 | class Level: 5 | word: str 6 | failed_attempts: int 7 | 8 | 9 | EASY = Level(word="SCRIPT", failed_attempts=10) 10 | MEDIUM = Level(word="PYTHONIC", failed_attempts=8) 11 | HARD = Level(word="INTERPRETER", failed_attempts=3) 12 | PESHO_BAFTATA = Level(word="ILLUMINATI", failed_attempts=1) -------------------------------------------------------------------------------- /14 - Testing/game/level.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | @dataclass(frozen=True) 4 | class Level: 5 | word: str 6 | failed_attempts: int 7 | 8 | 9 | EASY = Level(word="SCRIPT", failed_attempts=10) 10 | MEDIUM = Level(word="PYTHONIC", failed_attempts=8) 11 | HARD = Level(word="INTERPRETER", failed_attempts=3) 12 | PESHO_BAFTATA = Level(word="ILLUMINATI", failed_attempts=1) -------------------------------------------------------------------------------- /15 - Web programming/examples/templates/fancy_index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block styles %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

This is the home page

9 |

Hi, {{user}}

10 | 11 | {% endblock %} -------------------------------------------------------------------------------- /labs/lab06/solution-example/app/models/note.py: -------------------------------------------------------------------------------- 1 | """Note model definition.""" 2 | 3 | 4 | from dataclasses import dataclass 5 | 6 | 7 | @dataclass 8 | class Note: 9 | """Shared core dataclass representing a note.""" 10 | title: str 11 | content: str 12 | due_date: str | None = None # за простота ползваме `str`, 13 | # иначе най-удачно е `datetime` обект или `int` timestamp 14 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/app/ui/success_messages.py: -------------------------------------------------------------------------------- 1 | """Success messages for various commands.""" 2 | 3 | 4 | ADD_SUCCESS_MESSAGE = "Added new note '{title}'" # това е template string, който трябва да се 5 | # използва с `.format(title=...)` 6 | 7 | DELETE_SUCCESS_MESSAGE = "Deleted." 8 | 9 | EDIT_SUCCESS_MESSAGE = "Note successfully edited." 10 | -------------------------------------------------------------------------------- /12 - Modules/game/players/input_player.py: -------------------------------------------------------------------------------- 1 | from ..player import Player 2 | import string 3 | 4 | class InputPlayer(Player): 5 | def guess(self, state: str, eliminated_chars: set[str]) -> str: 6 | result = "_" 7 | allowed = set(string.ascii_uppercase) - eliminated_chars - set(state) - {"_"} 8 | while result not in allowed: 9 | result = input("Guess a character: ").upper() 10 | return result -------------------------------------------------------------------------------- /14 - Testing/game/players/input_player.py: -------------------------------------------------------------------------------- 1 | from ..player import Player 2 | import string 3 | 4 | class InputPlayer(Player): 5 | def guess(self, state: str, eliminated_chars: set[str]) -> str: 6 | result = "_" 7 | allowed = set(string.ascii_uppercase) - eliminated_chars - set(state) - {"_"} 8 | while result not in allowed: 9 | result = input("Guess a character: ").upper() 10 | return result -------------------------------------------------------------------------------- /labs/lab06/solution-example/app/ui/note_details.py: -------------------------------------------------------------------------------- 1 | """Note details view CLI output.""" 2 | 3 | 4 | from app.models.note import Note 5 | 6 | 7 | def note_details(note: Note) -> str: 8 | """Return a string representation of the note details.""" 9 | 10 | return f""" 11 | Title: {note.title} 12 | --- 13 | Content: {note.content} 14 | --- 15 | {"Due:"+note.due_date if note.due_date else "No due date."} 16 | """ -------------------------------------------------------------------------------- /price-tracker-demo/scraper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Scrape the price from a desktop.bg item 3 | """ 4 | import requests 5 | from bs4 import BeautifulSoup 6 | 7 | 8 | def get_price(url: str) -> float: 9 | page = requests.get(url) 10 | html_doc = page.content 11 | 12 | soup = BeautifulSoup(html_doc, "html.parser") 13 | 14 | # for item in soup.find_all("span", {"class": "price"}): 15 | # print(item) 16 | 17 | return 0.0 18 | -------------------------------------------------------------------------------- /project-setup-demo/game/players/input_player.py: -------------------------------------------------------------------------------- 1 | from ..player import Player 2 | import string 3 | 4 | class InputPlayer(Player): 5 | def guess(self, state: str, eliminated_chars: set[str]) -> str: 6 | result = "_" 7 | allowed = set(string.ascii_uppercase) - eliminated_chars - set(state) - {"_"} 8 | while result not in allowed: 9 | result = input("Guess a character: ").upper() 10 | return result -------------------------------------------------------------------------------- /12 - Modules/game/players/mock_player.py: -------------------------------------------------------------------------------- 1 | from game.player import Player 2 | 3 | class MockPlayer(Player): 4 | def __init__(self, initial_hp: int, guesses: str): 5 | self.guesses = guesses 6 | self.__index = 0 7 | super().__init__(initial_hp) 8 | 9 | def guess(self, state: str, eliminated_chars: set[str]) -> str: 10 | result = self.guesses[self.__index] 11 | self.__index += 1 12 | return result -------------------------------------------------------------------------------- /14 - Testing/tests/mocks/mock_player.py: -------------------------------------------------------------------------------- 1 | from game.player import Player 2 | 3 | class MockPlayer(Player): 4 | def __init__(self, initial_hp: int, guesses: str): 5 | self.guesses = guesses 6 | self.__index = 0 7 | super().__init__(initial_hp) 8 | 9 | def guess(self, state: str, eliminated_chars: set[str]) -> str: 10 | result = self.guesses[self.__index] 11 | self.__index += 1 12 | return result -------------------------------------------------------------------------------- /15 - Web programming/examples/config_example.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, make_response 2 | 3 | app = Flask(__name__) 4 | # app.config['ENV'] = 'development' 5 | 6 | @app.route("/") 7 | def home(): 8 | return make_response("Hello World!", 200, {"Debug": "Hello World!"}) 9 | 10 | if app.config['ENV'] == 'development': 11 | @app.route("/dev") 12 | def dev(): 13 | return make_response("This is the development panel !", 200) 14 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/app/ui/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Този пакет съдържа компонентите на потребителския интерфейс на приложението. 3 | 4 | Тъй като това е просто CLI приложение, 5 | потребителският интерфейс е реализиран с помощта на низове, които да бъдат принтирани. 6 | 7 | Тук може да бъде мястото за реализиране на по-сложни потребителски интерфейси, 8 | като GUI (например с tkinter / wxpython / pygame) или някакъв уеб интерфейс и т.н. 9 | """ 10 | -------------------------------------------------------------------------------- /project-setup-demo/game/players/mock_player.py: -------------------------------------------------------------------------------- 1 | from game.player import Player 2 | 3 | class MockPlayer(Player): 4 | def __init__(self, initial_hp: int, guesses: str): 5 | self.guesses = guesses 6 | self.__index = 0 7 | super().__init__(initial_hp) 8 | 9 | def guess(self, state: str, eliminated_chars: set[str]) -> str: 10 | result = self.guesses[self.__index] 11 | self.__index += 1 12 | return result -------------------------------------------------------------------------------- /15 - Web programming/examples/external_config_example.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, make_response 2 | 3 | app = Flask(__name__) 4 | app.config.from_envvar('FLASK_CONFIG') 5 | 6 | @app.route("/") 7 | def home(): 8 | return make_response("Hello World!", 200, {"Debug": "Hello World!"}) 9 | 10 | if app.config['ENV'] == 'development': 11 | @app.route("/dev") 12 | def dev(): 13 | return make_response("This is the development panel !", 200) 14 | -------------------------------------------------------------------------------- /12 - Modules/game/player.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | class Player(ABC): 4 | def __init__(self, initial_hp: int): 5 | self.hp = initial_hp 6 | 7 | def take_fail(self): 8 | self.hp = max(0, self.hp - 1) 9 | 10 | @property 11 | def is_dead(self): 12 | return self.hp <= 0 13 | 14 | @abstractmethod 15 | def guess(self, state: str, eliminated_chars: set[str]) -> str: 16 | ... 17 | 18 | -------------------------------------------------------------------------------- /14 - Testing/game/player.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | class Player(ABC): 4 | def __init__(self, initial_hp: int): 5 | self.hp = initial_hp 6 | 7 | def take_fail(self): 8 | self.hp = max(0, self.hp - 1) 9 | 10 | @property 11 | def is_dead(self): 12 | return self.hp <= 0 13 | 14 | @abstractmethod 15 | def guess(self, state: str, eliminated_chars: set[str]) -> str: 16 | ... 17 | 18 | -------------------------------------------------------------------------------- /project-setup-demo/game/player.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | class Player(ABC): 4 | def __init__(self, initial_hp: int): 5 | self.hp = initial_hp 6 | 7 | def take_fail(self): 8 | self.hp = max(0, self.hp - 1) 9 | 10 | @property 11 | def is_dead(self): 12 | return self.hp <= 0 13 | 14 | @abstractmethod 15 | def guess(self, state: str, eliminated_chars: set[str]) -> str: 16 | ... 17 | 18 | -------------------------------------------------------------------------------- /pywc/sample.txt: -------------------------------------------------------------------------------- 1 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 2 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 3 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 4 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." 5 | -------------------------------------------------------------------------------- /08 - Files/files/1.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 2 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 3 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 4 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum 5 | -------------------------------------------------------------------------------- /price-tracker-demo/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | CLI interface for the app 3 | """ 4 | 5 | import argparse 6 | 7 | 8 | def load_cli(): 9 | parser = argparse.ArgumentParser( 10 | prog="price-tracker-demo", 11 | description="A simple Python CLI app to track prices" 12 | ) 13 | 14 | parser.add_argument("-a", "--add", dest="item_url") 15 | parser.add_argument("-l", "--list", action='store_true') 16 | 17 | args = parser.parse_args() 18 | 19 | return args 20 | -------------------------------------------------------------------------------- /15 - Web programming/examples/custom_404.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, make_response, render_template 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/") 6 | def home(): 7 | if 'user' in request.headers: 8 | user = request.headers['user'] 9 | return make_response(f"Hi, {user}", 200) 10 | return make_response("Hi, Guest", 200) 11 | 12 | @app.errorhandler(404) 13 | def page_not_found(e): 14 | return make_response(render_template('custom_404.html'), 404) 15 | -------------------------------------------------------------------------------- /15 - Web programming/examples/simple_user_page.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | 3 | app = Flask(__name__) 4 | 5 | 6 | @app.route("/") 7 | def home(): 8 | return render_template('index.html', title='Home', user='Lyubo') 9 | 10 | 11 | @app.route("/login") 12 | def login(): 13 | return render_template('login.html', title='Login') 14 | 15 | 16 | @app.route("/user/") 17 | def user_page(username): 18 | return render_template('index.html', title='User', user=username) -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | title: Python Course 2024/2025 2 | author: Alexander Ignatov, Lyuboslav Karev, Karina Hristova, Alexandr Komarov, Ivan Luchev 3 | logo: misc/logo.png 4 | 5 | execute: 6 | execute_notebooks: off 7 | 8 | repository: 9 | url: https://github.com/fmipython/PythonCourse2024 10 | branch: master 11 | 12 | html: 13 | use_repository_button: true 14 | use_issues_button: true 15 | use_edit_page_button: true 16 | 17 | launch_buttons: 18 | colab_url: "https://colab.research.google.com" 19 | -------------------------------------------------------------------------------- /15 - Web programming/examples/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | 8 | {% block styles %} 9 | {% endblock %} 10 | 11 | {% block scripts %} 12 | {% endblock %} 13 | 14 | 15 |
16 | {% block content %} 17 | {% endblock %} 18 |
19 | 20 | -------------------------------------------------------------------------------- /15 - Web programming/examples/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

This is the login page

5 |
6 | 7 | 8 | 9 |
10 | 11 | {% if message %} 12 |

{{ message }}

13 | {% endif %} 14 | {% endblock %} -------------------------------------------------------------------------------- /labs/lab06/solution-example/app/ui/notes_list.py: -------------------------------------------------------------------------------- 1 | """All notes list CLI output.""" 2 | 3 | 4 | from app.models.note import Note 5 | 6 | 7 | def list_all_notes(notes: list[Note]) -> str: 8 | """Return a string representation of all notes in the list.""" 9 | 10 | header = "Listing notes..." 11 | 12 | if len(notes) == 0: 13 | return f"{header}\nNothing to list." 14 | 15 | return header + "\n" + "\n".join( 16 | f"- {note.title} (Due: {note.due_date})" 17 | for note in notes 18 | ) 19 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/app/storage/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пакет, съдържащ всички начини на съхранение на бележки. 3 | 4 | В момента има само един такъв: чрез JSON файл. 5 | 6 | В бъдеще могат да лесно бъдат добавени и други, като база данни и т.н. 7 | В такъв случай ще е удачно да бъде отделен общият им интерфейс в абстрактен базов клас (ABC), 8 | така че другите компоненти на приложението (в случая само `main`) да могат да съхраняват данните, 9 | без да знаят конкретните детайли на реализацията (Dependency Inversion принципът). 10 | """ 11 | -------------------------------------------------------------------------------- /price-tracker-demo/state.py: -------------------------------------------------------------------------------- 1 | """ 2 | Persistent state of the list 3 | """ 4 | from typing import List 5 | 6 | import json 7 | from item import Item 8 | 9 | STATE_FILEPATH = "state.json" 10 | 11 | 12 | def load_state() -> List[Item]: 13 | with open(STATE_FILEPATH, "r") as fp: 14 | items = json.load(fp) 15 | 16 | return [Item.deserialize(item) for item in items] 17 | 18 | 19 | def save_state(items: List[Item]): 20 | content = [item.serialize() for item in items] 21 | 22 | with open(STATE_FILEPATH, "w") as fp: 23 | json.dump(content, fp) 24 | -------------------------------------------------------------------------------- /project-setup-demo/config_parser/parser.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | 4 | class ConfigParser: 5 | def __init__(self, config_file: str) -> None: 6 | self.config_file = config_file 7 | self.config = {} 8 | 9 | def parse(self) -> None: 10 | with open(self.config_file, "r") as f: 11 | loaded = yaml.load(f, Loader=yaml.CLoader) 12 | self.config = loaded 13 | 14 | 15 | if __name__ == "__main__": 16 | # for testing purposes 17 | parser = ConfigParser("../configs/ai-easy.yaml") 18 | parser.parse() 19 | print(parser.config) 20 | -------------------------------------------------------------------------------- /12 - Modules/game/players/ai.py: -------------------------------------------------------------------------------- 1 | from ..player import Player 2 | 3 | class AI(Player): 4 | ORDERED_CHARS = "ETAOINSRHDLUCMFYWGPBVKXQJZ" 5 | 6 | def guess(self, state: str, eliminated_chars: set[str]) -> str: 7 | all_eliminated = { 8 | char.upper() 9 | for char in set(state) | eliminated_chars 10 | if char != "_" 11 | } 12 | 13 | for candidate in self.ORDERED_CHARS: 14 | if candidate not in all_eliminated: 15 | return candidate 16 | 17 | assert False, "No more characters to guess" -------------------------------------------------------------------------------- /14 - Testing/game/players/ai.py: -------------------------------------------------------------------------------- 1 | from ..player import Player 2 | 3 | class AI(Player): 4 | ORDERED_CHARS = "ETAOINSRHDLUCMFYWGPBVKXQJZ" 5 | 6 | def guess(self, state: str, eliminated_chars: set[str]) -> str: 7 | all_eliminated = { 8 | char.upper() 9 | for char in set(state) | eliminated_chars 10 | if char != "_" 11 | } 12 | 13 | for candidate in self.ORDERED_CHARS: 14 | if candidate not in all_eliminated: 15 | return candidate 16 | 17 | assert False, "No more characters to guess" -------------------------------------------------------------------------------- /project-setup-demo/game/players/ai.py: -------------------------------------------------------------------------------- 1 | from ..player import Player 2 | 3 | class AI(Player): 4 | ORDERED_CHARS = "ETAOINSRHDLUCMFYWGPBVKXQJZ" 5 | 6 | def guess(self, state: str, eliminated_chars: set[str]) -> str: 7 | all_eliminated = { 8 | char.upper() 9 | for char in set(state) | eliminated_chars 10 | if char != "_" 11 | } 12 | 13 | for candidate in self.ORDERED_CHARS: 14 | if candidate not in all_eliminated: 15 | return candidate 16 | 17 | assert False, "No more characters to guess" -------------------------------------------------------------------------------- /labs/lab06/solution-example/app/actions/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пакетът съдържа модули, които имплементират бизнес логиката на приложението, 3 | т.е. какво може да прави, как се очаква да се държи при еди-си-какви-си обстоятелства. 4 | 5 | В нашия случай решихме да направим функциите "чисти" (pure) - такива без странични ефекти, 6 | защото: 7 | 1. да бъдат тествани по-лесно 8 | 2. да не зависят от имплементационни детайли (JSON file-ове, print-ове, т.н.) 9 | 3. single responsibility 10 | 11 | В т.нар. clean architecture, такива класове/модули/функции могат обикновено се наричат Use Cases. 12 | """ 13 | -------------------------------------------------------------------------------- /16 - Using C code in Python/C/structs/rational.h: -------------------------------------------------------------------------------- 1 | #ifndef EXAMPLE_2_RATIONAL_H 2 | #define EXAMPLE_2_RATIONAL_H 3 | 4 | struct Rational{ 5 | int numerator; 6 | int denominator; 7 | }; 8 | 9 | extern struct Rational add(const struct Rational a, const struct Rational b); 10 | extern struct Rational subtract(const struct Rational a, const struct Rational b); 11 | extern struct Rational multiply(const struct Rational a, const struct Rational b); 12 | extern struct Rational divide(const struct Rational a, const struct Rational b); 13 | 14 | extern struct Rational build(const int a, const int b); 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /labs/lab06/spaghetti-notes/actions/list.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def list(): 4 | notes = None 5 | import os 6 | 7 | if os.path.exists("notes.json"): 8 | with open("notes.json", "r") as f: 9 | try: 10 | notes = json.load(f) 11 | except: 12 | notes = {} 13 | else: 14 | notes = {} 15 | 16 | print("Listing notes...") 17 | if len(notes) == 0: 18 | print("Nothing to list.") 19 | else: 20 | for title in notes: 21 | print("- " + title + " (Due: " + (notes[title]["due_date"] if notes[title]["due_date"] else "None") + ")") 22 | -------------------------------------------------------------------------------- /labs/lab06/spaghetti-notes/actions/delete.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def delete(title): 4 | notes = None 5 | import os 6 | 7 | if os.path.exists("notes.json"): 8 | with open("notes.json", "r") as f: 9 | try: 10 | notes = json.load(f) 11 | except: 12 | notes = {} 13 | else: 14 | notes = {} 15 | 16 | if title == "": 17 | print("Need title.") 18 | elif title not in notes: 19 | print("Doesn't exist, can't delete.") 20 | else: 21 | del notes[title] 22 | with open("notes.json", "w") as f: 23 | json.dump(notes, f) 24 | print("Deleted.") -------------------------------------------------------------------------------- /16 - Using C code in Python/C/arrays_pointers/dynamic_array.h: -------------------------------------------------------------------------------- 1 | #ifndef ARRAYS_POINTERS_H 2 | #define ARRAYS_POINTERS_H 3 | 4 | #include 5 | #include 6 | 7 | struct DynamicArray{ 8 | int* items; 9 | int capacity; 10 | int size; 11 | }; 12 | 13 | extern struct DynamicArray create(const int capacity); 14 | extern struct DynamicArray create_from_raw(const int* items, const int capacity); 15 | extern void add(struct DynamicArray* instance, const int item); 16 | extern int get(const struct DynamicArray* instance, const int index); 17 | extern void resize(struct DynamicArray* instance); 18 | extern void destruct(struct DynamicArray* instance); 19 | 20 | #endif -------------------------------------------------------------------------------- /price-tracker-demo/state.json: -------------------------------------------------------------------------------- 1 | ["{\"name\": \"https://desktop.bg/desktop_rams-corsair-16gb_2x8gb_ddr4_3200mhz_corsair_vengeance_rgb_pro\", \"url\": \"https://desktop.bg/desktop_rams-corsair-16gb_2x8gb_ddr4_3200mhz_corsair_vengeance_rgb_pro\", \"price\": 0.0}", "{\"name\": \"https://desktop.bg/desktop_rams-corsair-16gb_2x8gb_ddr4_3200mhz_corsair_vengeance_rgb_pro\", \"url\": \"https://desktop.bg/desktop_rams-corsair-16gb_2x8gb_ddr4_3200mhz_corsair_vengeance_rgb_pro\", \"price\": 0.0}", "{\"name\": \"https://desktop.bg/desktop_rams-corsair-16gb_2x8gb_ddr4_3200mhz_corsair_vengeance_rgb_pro\", \"url\": \"https://desktop.bg/desktop_rams-corsair-16gb_2x8gb_ddr4_3200mhz_corsair_vengeance_rgb_pro\", \"price\": 0.0}"] -------------------------------------------------------------------------------- /labs/lab06/spaghetti-notes/actions/view.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def view(title): 4 | notes = None 5 | import os 6 | 7 | if os.path.exists("notes.json"): 8 | with open("notes.json", "r") as f: 9 | try: 10 | notes = json.load(f) 11 | except: 12 | notes = {} 13 | else: 14 | notes = {} 15 | 16 | if title == "": 17 | print("Need title.") 18 | elif title not in notes: 19 | print("Doesn't exist.") 20 | else: 21 | print(title) 22 | print("---") 23 | print(notes[title]["content"]) 24 | print("---") 25 | if notes[title]["due_date"]: 26 | print("Due:" + notes[title]["due_date"]) 27 | else: 28 | print("No due date.") -------------------------------------------------------------------------------- /labs/lab06/solution-example/app/actions/delete.py: -------------------------------------------------------------------------------- 1 | """Delete command implementation.""" 2 | 3 | 4 | from app.models.note import Note 5 | 6 | from app.exceptions.validation import ( 7 | MissingTitleException, 8 | NoteNotFoundException, 9 | ) 10 | 11 | 12 | def delete(title: str, notes: dict[str, Note]) -> dict[str, Note]: 13 | """ 14 | Attempt to delete the note with the given title. 15 | 16 | Return the updated notes dictionary. 17 | 18 | Raises an appropriate `ValidationException` subclass 19 | if the title is missing or if the note with the given 20 | title does not exist. 21 | """ 22 | 23 | if not title: 24 | raise MissingTitleException 25 | 26 | if title not in notes: 27 | raise NoteNotFoundException(title) 28 | 29 | del notes[title] 30 | 31 | return notes 32 | -------------------------------------------------------------------------------- /project-setup-demo/README.md: -------------------------------------------------------------------------------- 1 | # Бесеница 2 | 3 | Проектът реализира игра на "Бесеница" с команден интерфейс. Позволява различни възможности за конфигурация, които включват нива на трудност и начин на игра (от истински играч или AI). 4 | 5 | ## Инсталация 6 | 7 | 1. Създаване на виртуална среда: 8 | 9 | ```bash 10 | python3 -m venv .venv 11 | source .venv/bin/activate 12 | pip install --upgrade pip 13 | ``` 14 | 15 | 2. Инсталация на небходимите пакети: 16 | 1. За употреба: 17 | 18 | ```bash 19 | pip install -r requirements.txt 20 | ``` 21 | 22 | 2. За разработка: 23 | 24 | ```bash 25 | pip install -r requirements-dev.txt 26 | ``` 27 | 28 | ## Употреба 29 | 30 | ```bash 31 | python3 main.py [name_of_config] 32 | ``` 33 | 34 | където `name_of_config` е името на конфигурационния YAML файл от директорията `configs` (без разширението). 35 | -------------------------------------------------------------------------------- /price-tracker-demo/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Price tracker CLI app 3 | 4 | - Add a page to track (URL/name) 5 | - List all tracked items and their current price 6 | - (Nice to have) Show history for an item 7 | """ 8 | from typing import List 9 | 10 | from cli import load_cli 11 | from item import Item 12 | from scraper import get_price 13 | from state import save_state, load_state 14 | 15 | def add_to_list(url: str, items: List[Item]): 16 | price = get_price(url) 17 | 18 | item = Item(url, url, price) 19 | 20 | items.append(item) 21 | 22 | 23 | if __name__ == "__main__": 24 | args = load_cli() 25 | 26 | items = load_state() 27 | 28 | if args.item_url is not None: 29 | # Add new item 30 | add_to_list(args.item_url, items) 31 | else: 32 | # List 33 | for item in items: 34 | print(item) 35 | 36 | save_state(items) 37 | -------------------------------------------------------------------------------- /labs/lab03/lab03.md: -------------------------------------------------------------------------------- 1 | # Задачи по Тема 5 (3т.) 2 | 3 | **Важно** Решенията трябва да бъдат в Python стип. Това означава използване на функционалности на езика - list comprehension, generators, slicing, enumerate, etc. 4 | Ако решението ви е по-дълго от 10-тина реда, вероятно не е в Питонски стил. Ще бъдат отнемани точки за стил. 5 | 6 | За да се зачете задачата ви, трябва да минават всички тестове в LeetCode. 7 | 8 | ## Задача 1 (0.5т) 9 | 10 | [Линк](https://leetcode.com/problems/valid-anagram/) 11 | 12 | ## Задача 2 (0.75т) 13 | 14 | [Линк](https://leetcode.com/problems/keyboard-row/) 15 | 16 | ## Задача 3 (0.5т) 17 | 18 | [Линк](https://leetcode.com/problems/majority-element/) 19 | 20 | ## Задача 4 (0.5т) 21 | 22 | [Линк](https://leetcode.com/problems/check-if-it-is-a-straight-line) 23 | 24 | ## Задача 5 (0.75т) 25 | 26 | [Линк](https://leetcode.com/problems/the-k-weakest-rows-in-a-matrix/) 27 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/tests/ui/test_note_details.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from app.models.note import Note 3 | from app.ui.note_details import note_details 4 | 5 | 6 | def test_note_details_with_due_date(): 7 | note = Note("Test Title", "Test Content", "2024-01-01") 8 | expected = """ 9 | Title: Test Title 10 | --- 11 | Content: Test Content 12 | --- 13 | Due:2024-01-01 14 | """ 15 | assert note_details(note) == expected 16 | 17 | def test_note_details_without_due_date(): 18 | note = Note("Test Title", "Test Content") 19 | expected = """ 20 | Title: Test Title 21 | --- 22 | Content: Test Content 23 | --- 24 | No due date. 25 | """ 26 | assert note_details(note) == expected 27 | 28 | def test_note_details_with_empty_content(): 29 | note = Note("Test Title", "") 30 | expected = """ 31 | Title: Test Title 32 | --- 33 | Content: 34 | --- 35 | No due date. 36 | """ 37 | assert note_details(note) == expected -------------------------------------------------------------------------------- /price-tracker-demo/item.py: -------------------------------------------------------------------------------- 1 | """ 2 | Class representing an item 3 | """ 4 | 5 | import json 6 | 7 | 8 | class Item: 9 | def __init__(self, name: str, url: str, price: float = 0.0): 10 | self.__name = name 11 | self.__url = url 12 | self.__price = price 13 | 14 | @property 15 | def name(self) -> str: 16 | return self.__name 17 | 18 | @property 19 | def url(self) -> str: 20 | return self.__url 21 | 22 | @property 23 | def price(self) -> float: 24 | return self.__price 25 | 26 | @price.setter 27 | def price(self, new_price: float): 28 | self.__price = new_price 29 | 30 | def __str__(self) -> str: 31 | return f'{self.__name}: {self.__price} ({self.__url})' 32 | 33 | def serialize(self) -> str: 34 | return json.dumps({'name': self.__name, 'url': self.__url, 'price': self.__price}) 35 | 36 | @staticmethod 37 | def deserialize(serialized: str) -> 'Item': 38 | deserialized = json.loads(serialized) 39 | 40 | return Item(**deserialized) 41 | -------------------------------------------------------------------------------- /12 - Modules/besenitsa.py: -------------------------------------------------------------------------------- 1 | from game.players.input_player import InputPlayer 2 | from game.engine import * 3 | import game.level as level 4 | 5 | DIFFICULTIES = [level.EASY, level.MEDIUM, level.HARD, level.PESHO_BAFTATA] 6 | 7 | def main(): 8 | print("Welcome to Besenitsa.") 9 | 10 | d = -1 11 | while d not in range(len(DIFFICULTIES)): 12 | try: 13 | d = int(input("Select level [0-3]: ")) 14 | except ValueError: 15 | print("Please enter a valid number (0, 1, 2 or 3).") 16 | 17 | level = DIFFICULTIES[d] 18 | player = InputPlayer(level.failed_attempts) 19 | engine = BesenitsaEngine(level.word, player) 20 | 21 | outcome = GameState.ONGOING 22 | while outcome == GameState.ONGOING: 23 | print(f"Word: {engine.masked_word}\nFailed attempts left: {player.hp}") 24 | outcome = engine.guess() 25 | 26 | if outcome == GameState.LOST: 27 | print("Sorry, you lost.") 28 | elif outcome == GameState.WON: 29 | print("Congrats! You won.") 30 | 31 | if __name__ == "__main__": 32 | main() 33 | -------------------------------------------------------------------------------- /16 - Using C code in Python/C++/dynamic_array.h: -------------------------------------------------------------------------------- 1 | class DynamicArray { 2 | public: 3 | DynamicArray(const int capacity); 4 | DynamicArray(const int* items, const int capacity); 5 | 6 | void add(const int item); 7 | int get(const int index) const; 8 | void resize(); 9 | 10 | ~DynamicArray(); 11 | private: 12 | int* items; 13 | int capacity; 14 | int size; 15 | }; 16 | 17 | extern "C" { 18 | DynamicArray* create(const int capacity) { 19 | return new DynamicArray(capacity); 20 | } 21 | DynamicArray* create_from_raw(const int* items, const int capacity) { 22 | return new DynamicArray(items, capacity); 23 | } 24 | void add(DynamicArray* instance, const int item) { 25 | instance->add(item); 26 | } 27 | int get(const DynamicArray* instance, const int index) { 28 | return instance->get(index); 29 | } 30 | void resize(DynamicArray* instance) { 31 | instance->resize(); 32 | } 33 | void destruct(DynamicArray* instance) { 34 | delete instance; 35 | } 36 | } -------------------------------------------------------------------------------- /labs/lab06/spaghetti-notes/actions/add.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def add(title, content, due_date): 5 | notes = None 6 | import os 7 | 8 | if os.path.exists("notes.json"): 9 | with open("notes.json", "r") as f: 10 | try: 11 | notes = json.load(f) 12 | except: 13 | notes = {} 14 | else: 15 | notes = {} 16 | 17 | if title == "": 18 | print("No title, can't add note.") 19 | elif content == "": 20 | print("No content, can't add note.") 21 | else: 22 | if title in notes: 23 | print("Already exists, won't overwrite.") 24 | else: 25 | if due_date: 26 | notes[title] = { 27 | "content": content, 28 | "due_date": due_date 29 | } 30 | else: 31 | notes[title] = { 32 | "content": content, 33 | } 34 | with open("notes.json", "w") as f: 35 | f.write(json.dumps(notes)) 36 | print("Added new note", title) -------------------------------------------------------------------------------- /labs/lab06/solution-example/tests/actions/test_delete.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from app.actions.delete import delete 3 | from app.models.note import Note 4 | from app.exceptions.validation import MissingTitleException, NoteNotFoundException 5 | 6 | 7 | def test_delete_removes_note(): 8 | notes = {"test": Note("test", "content")} 9 | result = delete("test", notes) 10 | assert "test" not in result 11 | 12 | def test_delete_raises_missing_title(): 13 | with pytest.raises(MissingTitleException): 14 | delete("", {}) 15 | 16 | def test_delete_raises_not_found(): 17 | with pytest.raises(NoteNotFoundException): 18 | delete("nonexistent", {}) 19 | 20 | def test_delete_returns_dict(): 21 | notes = {"test": Note("test", "content")} 22 | result = delete("test", notes) 23 | assert isinstance(result, dict) 24 | 25 | def test_delete_preserves_other_notes(): 26 | notes = { 27 | "test1": Note("test1", "content1"), 28 | "test2": Note("test2", "content2") 29 | } 30 | result = delete("test1", notes) 31 | assert "test2" in result 32 | assert result["test2"].content == "content2" -------------------------------------------------------------------------------- /15 - Web programming/examples/login_page.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, redirect, url_for 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/") 6 | def home(): 7 | return render_template('index.html', title='Home', user='Guest') 8 | 9 | 10 | @app.route("/login_action", methods=['POST']) 11 | def login_action(): 12 | if request.method != 'POST': 13 | return redirect(url_for('/login', message='Invalid method')) 14 | 15 | if request.form['username'] == 'admin' and request.form['password'] == 'admin': 16 | username = request.form['username'] 17 | return redirect(url_for('user_page', name=username)) 18 | else: 19 | return redirect(url_for('login', message='Invalid username or password')) 20 | 21 | 22 | @app.route("/login") 23 | def login(message=None): 24 | if 'message' in request.args: 25 | message = request.args['message'] 26 | print(message) 27 | return render_template('login.html', title='Login', message=message) 28 | 29 | 30 | @app.route("/user_page/") 31 | def user_page(name): 32 | return render_template('user.html', title='User', user=name) 33 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/app/models/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Т.нар. "модел" е общоприет термин в много architectural & design patterns, 3 | който обозначава просто клас (тип проста структура от данни), 4 | който репрезентира вярно и пълно цялата информация, с която борави приложението. 5 | 6 | В нашия случай, това е клас `Note`, който представя една бележка в нашето приложение. 7 | 8 | Причини: 9 | 10 | 1. По-лесна и необъркваща е работата с такива класове. 11 | 2. По-лесно е да се добавят нови полета и функционалности към класа, отколкото към речник. 12 | 3. Моделите на приложението не е хубаво да зависят от конкретните имлоементационни детайли, 13 | напр. на базата данни, 14 | на формата на файлове (JSON, CSV, XML, YAML, ...), 15 | на външни API-та и т.н. 16 | Най-удачно е, ако ползваме API, което предоставя обекти с малко по-различна структура от нашата, 17 | то да създадем отделни модели, който са валидни само за API-то, 18 | и да конвертираме между тях и нашите модели. 19 | 20 | Нашите модели (тези, които репрезентират core данните и логиката ни), 21 | се наричат **домейн модели**. Можем да имаме Database модели, Network модели и т.н. 22 | """ 23 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/app/actions/add.py: -------------------------------------------------------------------------------- 1 | """Add command implementation.""" 2 | 3 | 4 | from app.exceptions.validation import ( 5 | MissingContentException, 6 | MissingTitleException, 7 | NoteAlreadyExistsException, 8 | ) 9 | from app.models.note import Note 10 | 11 | 12 | def add( 13 | title: str, 14 | content: str, 15 | due_date: str | None, 16 | notes: dict[str, Note] 17 | ) -> dict[str, Note]: 18 | """ 19 | Attempt to create a new `Note` object with the given 20 | title, content, and due date (if provided). 21 | 22 | Return the updated notes dictionary. 23 | 24 | Raises an appropriate `ValidationException` subclass 25 | if the title is missing, the content is missing, 26 | or if the note with the given title already exists. 27 | """ 28 | 29 | if not title: 30 | raise MissingTitleException 31 | 32 | if not content: 33 | raise MissingContentException 34 | 35 | if title in notes: 36 | raise NoteAlreadyExistsException(title) 37 | 38 | notes[title] = Note( 39 | title=title, 40 | content=content, 41 | due_date=due_date or None, 42 | ) 43 | 44 | return notes 45 | -------------------------------------------------------------------------------- /13 - Clean code/example1.py: -------------------------------------------------------------------------------- 1 | def f1(): 2 | x = input("Type 1: ") 3 | y = input("Type 2: ") 4 | op = input("Choose +, -, *, /: ") 5 | 6 | if op == '+': 7 | try: 8 | r = int(x) + int(y) 9 | except: 10 | try: 11 | r = float(x) + float(y) 12 | except: 13 | r = x + y 14 | elif op == '-': 15 | try: 16 | r = int(x) - int(y) 17 | except: 18 | try: 19 | r = float(x) - float(y) 20 | except: 21 | print("Invalid!") 22 | return 23 | elif op == '*': 24 | try: 25 | r = int(x) * int(y) 26 | except: 27 | try: 28 | r = float(x) * float(y) 29 | except: 30 | print("Invalid!") 31 | return 32 | elif op == '/': 33 | try: 34 | r = int(x) / int(y) 35 | except: 36 | try: 37 | r = float(x) / float(y) 38 | except: 39 | print("Invalid!") 40 | return 41 | else: 42 | print("Bad operator!") 43 | return 44 | 45 | print("Result: ", r) 46 | 47 | f1() 48 | -------------------------------------------------------------------------------- /16 - Using C code in Python/C++/dynamic_array.cpp: -------------------------------------------------------------------------------- 1 | #include "dynamic_array.h" 2 | 3 | DynamicArray::DynamicArray(const int capacity): capacity(capacity), size(0) { 4 | this->items = new int[this->capacity]; 5 | } 6 | DynamicArray::DynamicArray(const int* items, const int capacity): capacity(capacity) { 7 | this->items = new int[this->capacity]; 8 | for (int i = 0; i < this->capacity; i++) { 9 | this->items[i] = items[i]; 10 | } 11 | this->size = this->capacity; 12 | } 13 | 14 | void DynamicArray::add(const int item) { 15 | if (this->size == this->capacity) { 16 | this->resize(); 17 | } 18 | this->items[this->size] = item; 19 | this->size++; 20 | } 21 | int DynamicArray::get(const int index) const { 22 | if (index < 0 || index >= this->size) { 23 | throw "Index out of bounds"; 24 | } 25 | return this->items[index]; 26 | } 27 | void DynamicArray::resize() { 28 | int* new_items = new int[this->capacity * 2]; 29 | for (int i = 0; i < this->capacity; i++) { 30 | new_items[i] = this->items[i]; 31 | } 32 | delete[] this->items; 33 | this->items = new_items; 34 | this->capacity *= 2; 35 | } 36 | 37 | DynamicArray::~DynamicArray() { 38 | delete[] this->items; 39 | } -------------------------------------------------------------------------------- /labs/lab06/spaghetti-notes/main.py: -------------------------------------------------------------------------------- 1 | from actions.add import add 2 | from actions.view import view 3 | from actions.delete import delete 4 | from actions.list import list 5 | 6 | 7 | try: 8 | import argparse 9 | parser = argparse.ArgumentParser(description="Command-line Note-Taking Application") 10 | parser.add_argument("action", choices=["add", "view", "delete", "list"], help="What do you want to do?") 11 | parser.add_argument("--title", help="Title of the note") 12 | parser.add_argument("--content", help="Content of the note (only for `add` action)") 13 | parser.add_argument("--due-date", help="Optional due date (only for `add` action)") 14 | args = parser.parse_args() 15 | 16 | if args.action == "add": 17 | if args.title is None: 18 | print("Need title.") 19 | elif args.content == None: 20 | print("Needs content.") 21 | else: 22 | add(args.title, args.content, args.due_date) 23 | elif args.action == "view": 24 | view(args.title) 25 | elif args.action == "delete": 26 | delete(args.title) 27 | elif args.action == "list": 28 | list() 29 | else: 30 | print("Invalid action.") 31 | except: 32 | pass # да не гърми нищо 33 | -------------------------------------------------------------------------------- /15 - Web programming/examples/protected_login_page.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, redirect, url_for 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/") 6 | def home(): 7 | return render_template('index.html', title='Home', user='Guest') 8 | 9 | 10 | @app.route("/login_action", methods=['POST']) 11 | def login_action(): 12 | if request.method != 'POST': 13 | return redirect(url_for('/login', message='Invalid method')) 14 | 15 | if request.form['username'] == 'admin' and request.form['password'] == 'admin': 16 | username = request.form['username'] 17 | return redirect(url_for('user_page', name=username, token='123456')) 18 | else: 19 | return redirect(url_for('login', message='Invalid username or password')) 20 | 21 | 22 | @app.route("/login") 23 | def login(message=None): 24 | if 'message' in request.args: 25 | message = request.args['message'] 26 | print(message) 27 | return render_template('login.html', title='Login', message=message) 28 | 29 | 30 | @app.route("/user_page/") 31 | def user_page(name): 32 | if 'token' not in request.args: 33 | return redirect(url_for('login', message='You must login first')) 34 | return render_template('user.html', title='User', user=name) 35 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/tests/ui/test_notes_list.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from app.models.note import Note 3 | from app.ui.notes_list import list_all_notes 4 | 5 | 6 | def test_list_all_notes_empty(): 7 | """Test listing empty notes list.""" 8 | notes = [] 9 | expected = "Listing notes...\nNothing to list." 10 | assert list_all_notes(notes) == expected 11 | 12 | 13 | def test_list_all_notes_single(): 14 | """Test listing single note.""" 15 | notes = [Note("Test note", "arbitrary content")] 16 | expected = "Listing notes...\n- Test note (Due: None)" 17 | assert list_all_notes(notes) == expected 18 | 19 | 20 | def test_list_all_notes_multiple(): 21 | """Test listing multiple notes.""" 22 | titles = ["First note", "Second note", "Third note"] 23 | due_dates = [ 24 | "2024-01-01", 25 | None, 26 | "2024-12-31", 27 | ] 28 | notes = [ 29 | Note(title, "arbitrary content", due_date) 30 | for title, due_date in zip(titles, due_dates) 31 | 32 | ] 33 | expected = ( 34 | "Listing notes...\n" 35 | f"- First note (Due: {due_dates[0]})\n" 36 | f"- Second note (Due: {due_dates[1]})\n" 37 | f"- Third note (Due: {due_dates[2]})" 38 | ) 39 | assert list_all_notes(notes) == expected -------------------------------------------------------------------------------- /project-setup-demo/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from game.players.input_player import InputPlayer 4 | from game.players.ai import AI 5 | from game.engine import * 6 | from game.level import Level 7 | 8 | from config_parser.parser import ConfigParser 9 | 10 | import sys 11 | import os 12 | 13 | def main(): 14 | print("Welcome to Besenitsa.") 15 | 16 | config_name = sys.argv[1] 17 | 18 | path = os.path.join("configs", f"{config_name}.yaml") 19 | parser = ConfigParser(path) 20 | parser.parse() 21 | config_dict = parser.config 22 | 23 | is_ai = config_dict["ai_player"] 24 | level_props = config_dict["level"] 25 | level = Level(**level_props) 26 | 27 | if is_ai: 28 | player = AI(level.failed_attempts) 29 | else: 30 | player = InputPlayer(level.failed_attempts) 31 | 32 | engine = BesenitsaEngine(level.word, player) 33 | 34 | outcome = GameState.ONGOING 35 | while outcome == GameState.ONGOING: 36 | print(f"Word: {engine.masked_word}\nFailed attempts left: {player.hp}") 37 | outcome = engine.guess() 38 | 39 | if outcome == GameState.LOST: 40 | print("Sorry, you lost.") 41 | elif outcome == GameState.WON: 42 | print("Congrats! You won.") 43 | 44 | if __name__ == "__main__": 45 | main() 46 | -------------------------------------------------------------------------------- /.github/workflows/renderbook.yml: -------------------------------------------------------------------------------- 1 | name: Render JupyterBook 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | - setup_jupyter_book 9 | workflow_dispatch: # allow manual triggers 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow one concurrent deployment 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | render: 24 | environment: 25 | name: github-pages 26 | url: ${{ steps.deployment.outputs.page_url }} 27 | 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | 33 | - name: Install dependencies 34 | run: | 35 | python3 -m pip install --upgrade pip 36 | pip3 install jupyter-book 37 | 38 | - name: Build JupyterBook 39 | run: | 40 | jupyter-book build . 41 | 42 | - name: Setup Pages 43 | uses: actions/configure-pages@v2 44 | 45 | - name: Upload artifact 46 | uses: actions/upload-pages-artifact@v1 47 | with: 48 | path: '_build/html' 49 | 50 | - name: Deploy to GitHub Pages 51 | id: deployment 52 | uses: actions/deploy-pages@v1 53 | -------------------------------------------------------------------------------- /example_projects.md: -------------------------------------------------------------------------------- 1 | # Отлични проекти от минали години: 2 | 3 | ## Конзолни приложения / библиотеки 4 | - [Password manager, 2022/2023](https://github.com/lyudmilstamenov/password_manager) 5 | - [Poker API, 2022/2023](https://github.com/rosenkolev1/Python-Project) 6 | - [Job Ads Analyzer, 2023/2024](https://github.com/SemirBaldzhiev/Job-Ads-Analyzer) 7 | 8 | ## Desktop приложения / библиотеки 9 | - [Pytomation, 2022/2023](https://github.com/kristian3551/Pytomaton) 10 | - [Prolog interpreter, 2022/2023](https://github.com/HeavyHelium/python-course-project) 11 | - [PySimpleImageEditor, 2023/2024](https://github.com/PoinP/PySimpleImageEditor) 12 | 13 | ## Уебсайтове 14 | 15 | - с Flask: 16 | - [Instagram clone, 2022/2023](https://github.com/GerganaAngelova02/Instagram_clone_app) 17 | 18 | - с Django: 19 | - [Schule (електронен дневник), 2022/2023](https://github.com/karinaghristova/Schule) 20 | - [Scribble, 2022/2023](https://github.com/arctfx/scribble) 21 | - [Fitness-App, 2023/2024](https://github.com/nataliaNikolova13/Fitness-App) 22 | 23 | ## Игри: 24 | - [My-Bad-Bad-Ice-Cream, 2022/2023](https://github.com/YoanaAneva/My-Bad-Bad-Ice-Cream) 25 | - [Triviador clone, 2022/2023](https://github.com/vladi2703/triviador-clone) 26 | - [CubeMatic, 2022/2023](https://github.com/RylaD303/CubeMatic) 27 | - [Minesweeper, 2022/2023](https://github.com/toduko/minesweeper) 28 | - [Pandemic, 2023/2024](https://github.com/PokerFaz/Pandemic/tree/master) 29 | - [noosu!, 2023/2024](https://github.com/bymihaylov/noosu) -------------------------------------------------------------------------------- /16 - Using C code in Python/C/arrays_pointers/dynamic_array.c: -------------------------------------------------------------------------------- 1 | #include "dynamic_array.h" 2 | 3 | struct DynamicArray create(const int capacity) { 4 | struct DynamicArray instance; 5 | 6 | instance.capacity = capacity; 7 | instance.size = 0; 8 | instance.items = malloc(sizeof(int) * capacity); 9 | 10 | return instance; 11 | } 12 | 13 | struct DynamicArray create_from_raw(const int* items, const int capacity) { 14 | struct DynamicArray instance; 15 | 16 | instance.capacity = capacity; 17 | instance.size = capacity; 18 | instance.items = malloc(sizeof(int) * capacity); 19 | 20 | for (int i = 0; i < capacity; i++) { 21 | instance.items[i] = items[i]; 22 | } 23 | 24 | return instance; 25 | } 26 | 27 | void add(struct DynamicArray* instance, const int item) { 28 | if (instance->capacity == instance->size) { 29 | resize(instance); 30 | } 31 | 32 | instance->items[instance->size++] = item; 33 | } 34 | 35 | int get(const struct DynamicArray* instance, const int index) { 36 | if (index >= instance->size || index < 0) { 37 | printf("Index %d out of bounds for size %d", index, instance->size); 38 | } 39 | 40 | return instance->items[index]; 41 | } 42 | 43 | void resize(struct DynamicArray* instance) { 44 | instance->capacity *= 2; 45 | instance->items = realloc(instance->items, sizeof(int) * instance->capacity); 46 | } 47 | 48 | void destruct(struct DynamicArray* instance) { 49 | free(instance->items); 50 | } 51 | -------------------------------------------------------------------------------- /14 - Testing/game/engine.py: -------------------------------------------------------------------------------- 1 | from game.player import Player 2 | 3 | from enum import Enum 4 | import string 5 | 6 | 7 | class GameState(Enum): 8 | ONGOING = 0 9 | WON = 1 10 | LOST = 2 11 | 12 | 13 | class BesenitsaEngine: 14 | def __init__(self, word: str, player: Player): 15 | if len(word) < 3: 16 | raise ValueError("The word must have at least 3 characters!") 17 | 18 | if any(map(lambda c: c.upper() not in string.ascii_uppercase, word)): 19 | raise ValueError("The word must contain only ASCII letters!") 20 | 21 | self.player = player 22 | self.word = word.upper() 23 | self.revealed = {self.word[0], self.word[-1]} 24 | self.__update_masked_word() 25 | 26 | def __update_masked_word(self): 27 | self.masked_word = "".join( 28 | char if char in self.revealed else "_" 29 | for char in self.word 30 | ) 31 | 32 | def guess(self) -> GameState: 33 | attempt = self.player.guess(self.masked_word, self.revealed).upper() 34 | 35 | assert attempt not in self.revealed, "Attempted to guess a char again." 36 | 37 | self.revealed.add(attempt) 38 | self.__update_masked_word() 39 | 40 | if attempt not in self.word: 41 | self.player.take_fail() 42 | if self.player.is_dead: 43 | return GameState.LOST 44 | elif "_" not in self.masked_word: 45 | return GameState.WON 46 | 47 | return GameState.ONGOING 48 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/app/actions/edit.py: -------------------------------------------------------------------------------- 1 | """Edit command implementation.""" 2 | 3 | 4 | from app.exceptions.validation import ( 5 | MissingContentOrDueDateException, 6 | MissingTitleException, 7 | NoteNotFoundException, 8 | ) 9 | from app.models.note import Note 10 | 11 | 12 | def edit( 13 | title: str, 14 | content: str | None, 15 | due_date: str | None, 16 | notes: dict[str, Note] 17 | ) -> dict[str, Note]: 18 | """ 19 | Attempt to create a new `Note` object with the given 20 | title, content, and due date (if provided). 21 | 22 | If `due_date` is `"none"` then the due date is reset to None. 23 | 24 | If `due_date` or `content` is not provided, 25 | the existing note's value of the corresp. field is preserved. 26 | 27 | Return the updated notes dictionary. 28 | 29 | Raises an appropriate `ValidationException` subclass 30 | if the title is missing, both content and die date are missing, 31 | or if the note with the given title does not exist. 32 | """ 33 | 34 | if not title: 35 | raise MissingTitleException 36 | 37 | if not content and not due_date: 38 | raise MissingContentOrDueDateException 39 | 40 | if title not in notes: 41 | raise NoteNotFoundException(title) 42 | 43 | existing_note = notes[title] 44 | 45 | if due_date is None: 46 | new_due_date = existing_note.due_date 47 | elif due_date.strip().lower() == "none": 48 | new_due_date = None 49 | else: 50 | new_due_date = due_date 51 | 52 | notes[title] = Note( 53 | title=title, 54 | content=content or existing_note.content, 55 | due_date=new_due_date, 56 | ) 57 | 58 | return notes 59 | -------------------------------------------------------------------------------- /13 - Clean code/example1_refactored.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | COMMON_OPERATIONS = { 4 | "+": lambda x, y: x + y, 5 | "-": lambda x, y: x - y, 6 | "*": lambda x, y: x * y, 7 | } 8 | 9 | INT_OPERATIONS = { 10 | **COMMON_OPERATIONS, 11 | "/": lambda x, y: x // y, 12 | } 13 | 14 | FLOAT_OPERATIONS = { 15 | **COMMON_OPERATIONS, 16 | "/": lambda x, y: x / y, 17 | } 18 | 19 | 20 | def apply_operation_to_ints(x: int, y: int, operation: str): 21 | if operation not in INT_OPERATIONS: 22 | raise ValueError("Invalid operation") 23 | 24 | return INT_OPERATIONS[operation](x, y) 25 | 26 | 27 | def apply_operation_to_float(x: float, y: float, operation: str): 28 | if operation not in FLOAT_OPERATIONS: 29 | raise ValueError("Invalid operation") 30 | 31 | return FLOAT_OPERATIONS[operation](x, y) 32 | 33 | 34 | def parse_number(number: str) -> int | float: 35 | match number: 36 | case ".": 37 | return float(number) 38 | case _: 39 | return int(number) 40 | 41 | 42 | if __name__ == "__main__": 43 | x = input("First number: ") 44 | try: 45 | x = parse_number(x) 46 | except ValueError: 47 | print("Invalid value", file=sys.stderr) 48 | sys.exit(1) 49 | 50 | try: 51 | y = parse_number(input("Second number: ")) 52 | except ValueError: 53 | print("Invalid value", file=sys.stderr) 54 | sys.exit(1) 55 | 56 | operation = input("Choose +, -, *, /: ") 57 | 58 | try: 59 | result = apply_operation_to_ints(x, y, operation) 60 | except ValueError as exc: 61 | print(exc, file=sys.stderr) 62 | sys.exit(1) 63 | except ZeroDivisionError as exc: 64 | print(exc, file=sys.stderr) 65 | sys.exit(1) 66 | 67 | print(result) 68 | -------------------------------------------------------------------------------- /pywc/wc.py: -------------------------------------------------------------------------------- 1 | """ 2 | -l => lines 3 | -c => characters 4 | -w => words 5 | empty => all 3 6 | """ 7 | 8 | import argparse 9 | 10 | 11 | def parse_args() -> tuple[list[str], str]: 12 | parser = argparse.ArgumentParser( 13 | description="Python implementation of the core-utils wc" 14 | ) 15 | parser.add_argument("-l", "--lines", action="store_true") 16 | parser.add_argument("-w", "--words", action="store_true") 17 | parser.add_argument("-c", "--characters", action="store_true") 18 | parser.add_argument("filepath", type=str) 19 | 20 | args = parser.parse_args() 21 | 22 | return args 23 | 24 | 25 | def count_lines(content: list[str]) -> int: 26 | return len(content) 27 | 28 | 29 | def count_words(content: list[str]) -> int: 30 | words = get_words(content) 31 | return len(words) 32 | 33 | 34 | def get_words(content): 35 | words = sum([line.split() for line in content], []) 36 | words = [word.strip() for word in words] 37 | return words 38 | 39 | 40 | def count_characters(content: list[str]): 41 | words = get_words(content) 42 | characters = [len(word) for word in words] 43 | return sum(characters) 44 | 45 | 46 | if __name__ == "__main__": 47 | args = parse_args() 48 | 49 | with open(args.filepath, "r") as fp: 50 | lines = fp.readlines() 51 | 52 | lines_count = count_lines(lines) 53 | words_count = count_words(lines) 54 | characters_count = count_characters(lines) 55 | 56 | if args.lines: 57 | print(lines_count, end=" ") 58 | if args.words == "-w": 59 | print(words_count, end=" ") 60 | if args.characters == "-c": 61 | print(characters_count, end=" ") 62 | if not any((args.lines, args.words, args.characters)): 63 | print(lines_count, words_count, characters_count) 64 | -------------------------------------------------------------------------------- /03 - OOP/example1.py: -------------------------------------------------------------------------------- 1 | import math 2 | from abc import ABC, abstractmethod 3 | 4 | 5 | class Shape(ABC): 6 | def __init__(self, color): 7 | self._color = color 8 | 9 | @property 10 | def color(self): 11 | return self._color 12 | 13 | @abstractmethod 14 | def area(self): 15 | pass 16 | 17 | 18 | class Rectangle(Shape): 19 | def __init__(self, color, width, height): 20 | super().__init__(color) 21 | 22 | self._width = width 23 | self._height = height 24 | 25 | @property 26 | def width(self): 27 | return self._width 28 | 29 | @property 30 | def height(self): 31 | return self._height 32 | 33 | def area(self): 34 | return self.width * self.height 35 | 36 | 37 | class Circle(Shape): 38 | def __init__(self, color, radius): 39 | super().__init__(color) 40 | 41 | self._radius = radius 42 | 43 | @property 44 | def radius(self): 45 | return self._radius 46 | 47 | def area(self): 48 | return self.radius ** 2 * math.pi 49 | 50 | class Shapes: 51 | def __init__(self, shapes=None): 52 | shapes = [] if shapes is None else shapes 53 | self._shapes = shapes 54 | 55 | def add_new_shape(self, shape): 56 | self._shapes.append(shape) 57 | 58 | def area_of_all_rectangles(self): 59 | return self.__area_of(Rectangle) 60 | 61 | def area_of_all_circle(self): 62 | return self.__area_of(Circle) 63 | 64 | def __area_of(self, shape_type): 65 | result = 0 66 | for shape in self._shapes: 67 | if isinstance(shape, shape_type): 68 | result += shape.area 69 | 70 | return result 71 | 72 | def __getitem__(self, index): 73 | return self._shapes[index] 74 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/tests/actions/test_edit.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from app.actions.edit import edit 3 | from app.models.note import Note 4 | 5 | from app.exceptions.validation import ( 6 | MissingContentOrDueDateException, 7 | MissingTitleException, 8 | NoteNotFoundException, 9 | ) 10 | 11 | 12 | def test_edit_with_missing_title(): 13 | with pytest.raises(MissingTitleException): 14 | edit("", "content", "due date", {}) 15 | 16 | def test_edit_with_missing_content_and_due_date(): 17 | with pytest.raises(MissingContentOrDueDateException): 18 | edit("title", None, None, {"title": Note("title", "content", "due date")}) 19 | 20 | def test_edit_nonexistent_note(): 21 | with pytest.raises(NoteNotFoundException): 22 | edit("title", "content", "due date", {}) 23 | 24 | def test_edit_content_only(): 25 | notes = {"title": Note("title", "old content", "due date")} 26 | result = edit("title", "new content", None, notes) 27 | assert result["title"].content == "new content" 28 | assert result["title"].due_date == "due date" 29 | 30 | def test_edit_due_date_only(): 31 | notes = {"title": Note("title", "content", "old due date")} 32 | result = edit("title", None, "new due date", notes) 33 | assert result["title"].content == "content" 34 | assert result["title"].due_date == "new due date" 35 | 36 | def test_edit_remove_due_date(): 37 | notes = {"title": Note("title", "content", "due date")} 38 | result = edit("title", None, "none", notes) 39 | assert result["title"].due_date is None 40 | 41 | def test_edit_both_fields(): 42 | notes = {"title": Note("title", "old content", "old due date")} 43 | result = edit("title", "new content", "new due date", notes) 44 | assert result["title"].content == "new content" 45 | assert result["title"].due_date == "new due date" -------------------------------------------------------------------------------- /labs/lab06/solution-example/app/exceptions/validation.py: -------------------------------------------------------------------------------- 1 | """This module contains custom exceptions for validation errors""" 2 | 3 | 4 | class ValidationException(Exception): 5 | """Just a base class for all validation exceptions 6 | so that one can easily catch 'em all pokemons""" 7 | def __init__(self, message): 8 | self.message = message 9 | super().__init__(message) 10 | 11 | 12 | class MissingTitleException(ValidationException): 13 | """Raised when the user has not provided a title for the note""" 14 | def __init__(self): 15 | super().__init__("Title is required.") 16 | 17 | class MissingContentException(ValidationException): 18 | """Raised when the user has not provided content for the note""" 19 | def __init__(self): 20 | super().__init__("Content is required.") 21 | 22 | class MissingContentOrDueDateException(ValidationException): 23 | """Raised when the user has not provided content or due date for the note, 24 | when trying to edit a note.""" 25 | def __init__(self): 26 | super().__init__("No content or due date provided - no changes made to the note.") 27 | 28 | class InvalidActionException(ValidationException): 29 | """Raised when the user has provided an invalid action""" 30 | def __init__(self): 31 | super().__init__("Invalid action.") 32 | 33 | class NoteNotFoundException(ValidationException): 34 | """Raised when the note with the given title is not found""" 35 | def __init__(self, title): 36 | super().__init__(f"Note with title '{title}' not found.") 37 | 38 | class NoteAlreadyExistsException(ValidationException): 39 | """Raised when the note with the given title already exists, 40 | e.g. when trying to add a note with a title that is already taken""" 41 | def __init__(self, title): 42 | super().__init__(f"Note with title '{title}' already exists.") 43 | -------------------------------------------------------------------------------- /16 - Using C code in Python/C/structs/rational.c: -------------------------------------------------------------------------------- 1 | #include "rational.h" 2 | 3 | 4 | struct Rational build(const int a, const int b) { 5 | struct Rational result; 6 | result.numerator = a; 7 | result.denominator = b; 8 | return result; 9 | } 10 | 11 | struct Rational add(const struct Rational a, const struct Rational b) { 12 | int result_numerator = 0; 13 | int result_denominator = 0; 14 | 15 | if (a.denominator != b.denominator) { 16 | result_numerator = a.numerator * b.denominator + b.numerator * a.denominator; 17 | result_denominator = a.denominator * b.denominator; 18 | } 19 | else { 20 | result_numerator = a.numerator + b.numerator; 21 | result_denominator = a.denominator; 22 | } 23 | 24 | return build(result_numerator, result_denominator); 25 | } 26 | 27 | struct Rational subtract(const struct Rational a, const struct Rational b) { 28 | int result_numerator = 0; 29 | int result_denominator = 0; 30 | 31 | if (a.denominator != b.denominator) { 32 | result_numerator = a.numerator * b.denominator - b.numerator * a.denominator; 33 | result_denominator = a.denominator * b.denominator; 34 | } 35 | else { 36 | result_numerator = a.numerator + b.numerator; 37 | result_denominator = a.denominator; 38 | } 39 | 40 | return build(result_numerator, result_denominator); 41 | } 42 | 43 | struct Rational multiply(const struct Rational a, const struct Rational b) { 44 | struct Rational result; 45 | 46 | result.numerator = a.numerator * b.numerator; 47 | result.denominator = a.denominator * b.denominator; 48 | 49 | return result; 50 | } 51 | 52 | struct Rational divide(const struct Rational a, const struct Rational b) { 53 | struct Rational result; 54 | 55 | result.numerator = a.numerator * b.denominator; 56 | result.denominator = a.denominator * b.numerator; 57 | 58 | return result; 59 | } -------------------------------------------------------------------------------- /14 - Testing/tests/test_engine.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from tests.mocks.mock_player import MockPlayer 4 | from game.engine import BesenitsaEngine, GameState 5 | 6 | class EngineTests(unittest.TestCase): 7 | def test_initialWord_isMaskedCorrectly(self): 8 | # Arrange 9 | player = MockPlayer(1, "a") 10 | sut = BesenitsaEngine("cat", player) 11 | expected = "C_T" 12 | 13 | # Act 14 | result = sut.masked_word 15 | 16 | # Assert 17 | self.assertEqual(result, expected) 18 | 19 | def test_cat_win(self): 20 | # Arrange 21 | player_win = MockPlayer(1, "a") 22 | sut = BesenitsaEngine("cat", player_win) 23 | 24 | # Act 25 | result = sut.guess() 26 | 27 | # Assert 28 | self.assertEqual(result, GameState.WON) 29 | 30 | def test_cat_lose(self): 31 | # Arrange 32 | player_win = MockPlayer(1, "b") 33 | sut = BesenitsaEngine("cat", player_win) 34 | 35 | # Act 36 | result = sut.guess() 37 | 38 | # Assert 39 | self.assertEqual(result, GameState.LOST) 40 | 41 | def test_foobar_win(self): 42 | player_win = MockPlayer(1, "oba") 43 | engine_win = BesenitsaEngine("foobar", player_win) 44 | 45 | self.assertEqual(engine_win.guess(), GameState.ONGOING) 46 | self.assertEqual(engine_win.guess(), GameState.ONGOING) 47 | self.assertEqual(engine_win.guess(), GameState.WON) 48 | 49 | def test_foobar_lose(self): 50 | player_lose = MockPlayer(3, "asdg") 51 | engine_lose = BesenitsaEngine("foobar", player_lose) 52 | 53 | self.assertEqual(engine_lose.guess(), GameState.ONGOING) 54 | self.assertEqual(engine_lose.guess(), GameState.ONGOING) 55 | self.assertEqual(engine_lose.guess(), GameState.ONGOING) 56 | self.assertEqual(engine_lose.guess(), GameState.LOST) 57 | 58 | if __name__ == '__main__': 59 | unittest.main() -------------------------------------------------------------------------------- /labs/lab06/solution-example/app/storage/json_file.py: -------------------------------------------------------------------------------- 1 | """JSON file storage module.""" 2 | 3 | 4 | import json 5 | import os 6 | from typing import Any 7 | 8 | from app.models.note import Note 9 | 10 | 11 | _CONTENT_KEY = "content" 12 | _DUE_DATE_KEY = "due_date" 13 | 14 | _ENCODING = "utf-8" 15 | 16 | class JsonFileStorage: 17 | """Notes storage using a JSON file.""" 18 | 19 | def __init__(self, file_path: str): 20 | self.__file_path = file_path 21 | 22 | def load_all_notes(self) -> dict[str, Note]: 23 | """Return all persisted notes in the form of a dictionary 24 | with the note title as key and the `Note` object as value. 25 | """ 26 | if not os.path.exists(self.__file_path): 27 | return {} 28 | 29 | with open(self.__file_path, "r", encoding=_ENCODING) as file: 30 | json_dict = json.load(file) 31 | 32 | results = self.__json_to_notes_dict(json_dict) 33 | return results 34 | 35 | def save_all_notes(self, notes_dict: dict[str, Note]): 36 | """Persist all notes to the file.""" 37 | 38 | json_dict = self.__notes_dict_to_json(notes_dict) 39 | with open(self.__file_path, "w", encoding=_ENCODING) as file: 40 | json.dump(json_dict, file) 41 | 42 | 43 | def __json_to_notes_dict(self, json_data: dict[str, Any]) -> dict[str, Note]: 44 | return { 45 | title: Note( 46 | title=title, 47 | content=note_data[_CONTENT_KEY], 48 | due_date=note_data.get(_DUE_DATE_KEY), 49 | ) 50 | for title, note_data in json_data.items() 51 | } 52 | 53 | def __notes_dict_to_json(self, notes_dict: dict[str, Note]) -> dict[str, Any]: 54 | return { 55 | title: { 56 | _CONTENT_KEY: note.content, 57 | _DUE_DATE_KEY: note.due_date, 58 | } 59 | for title, note in notes_dict.items() 60 | } 61 | -------------------------------------------------------------------------------- /03 - OOP/example2.py: -------------------------------------------------------------------------------- 1 | class Counter: 2 | def __init__(self, initial=0, step=1): 3 | self._initial = initial 4 | self._step = step 5 | self._counter = initial 6 | 7 | def increment(self): 8 | self._counter += self._step 9 | 10 | @property 11 | def total(self): 12 | return self._counter 13 | 14 | @property 15 | def step(self): 16 | return self._step 17 | 18 | class TwoWayCounter(Counter): 19 | def __init__(self, initial=0, step=1): 20 | super().__init__(initial, step) 21 | 22 | def decrement(self): 23 | self._counter -= self._step 24 | 25 | class LimitedCounter(Counter): 26 | def __init__(self, max, initial=0, step=1): 27 | super().__init__(initial, step) 28 | self._max = max 29 | 30 | def increment(self): 31 | if self.total < self._max: 32 | super().increment() 33 | 34 | @property 35 | def get_max(self): 36 | return self._max 37 | 38 | class LimitedTwoWayCounter(TwoWayCounter, LimitedCounter): 39 | def __init__(self, min, max, initial=0, step=1): 40 | super().__init__(initial, step) 41 | #super(TwoWayCounter, self) to access LimitedCounter 42 | super(TwoWayCounter, self).__init__(max, initial, step) 43 | self._min = min 44 | 45 | def increment(self): 46 | super(TwoWayCounter, self).increment() 47 | 48 | def decrement(self): 49 | if self.total > self._min: 50 | super().decrement() 51 | 52 | @property 53 | def get_min(self): 54 | return self._min 55 | 56 | 57 | class Semaphore(LimitedTwoWayCounter): 58 | def __init__(self, is_available=False): 59 | initial = 1 if is_available else 0 60 | super().__init__(0, 1, initial, 1) 61 | 62 | c = LimitedTwoWayCounter(0, 10, 0, 1) 63 | c.increment() 64 | c.increment() 65 | print(c.total) 66 | c.increment() 67 | print(c.total) 68 | c.decrement() 69 | print(c.total) 70 | 71 | -------------------------------------------------------------------------------- /_toc.yml: -------------------------------------------------------------------------------- 1 | format: jb-book 2 | 3 | root: README 4 | 5 | parts: 6 | - caption: Лекции 7 | chapters: 8 | - file: "01 - Intro to Python/install-n-setup" 9 | - file: "01 - Intro to Python/notebooks" 10 | - file: "02 - Variables, types, control flow/02 - Variables, types, control flow" 11 | - file: "03 - OOP/03 - OOP" 12 | - file: "04 - Functional Programming/04 - Functional Programming" 13 | - file: "05 - Data Structures and Oddities/05 - Data Structures and Oddities" 14 | - file: "06 - Typing Hints/06 - Typing Hints" 15 | - file: "07 - Exceptions Handling/07 - Exceptions Handling" 16 | - file: "08 - Files/08 - Files" 17 | - file: "09 - Multithreading/09 - Multithreading" 18 | - file: "10 - requests/10 - requests" 19 | - file: "11 - Git/README" 20 | - file: "12 - Modules/13 - Modules" 21 | - file: "13 - Clean code/14 - Clean code in Python" 22 | - file: "14 - Testing/15 - Testing" 23 | - file: "15 - Web programming/16 - Web programming" 24 | - file: "16 - Using C code in Python/17 - Using C code in Python" 25 | - file: "17 - numpy, pandas, matplotlib/18 - numpy, pandas, matplotlib" 26 | 27 | - caption: Упражнения 28 | chapters: 29 | - file: labs/lab01 30 | title: Упражнение 1 (теми 2 & 3) 31 | - file: labs/lab02 32 | title: Упражнение 2 (тема 4) 33 | - file: labs/lab03 34 | title: Упражнение 3 (тема 5) 35 | - file: labs/lab04 36 | title: Упражнение 4 (теми 6, 7 & 8) 37 | - file: labs/lab05/README 38 | title: Упражнение 5 (теми 9, 10, 11 & 12) 39 | - file: labs/lab06/README 40 | title: Упражнение 6 (теми 13 и 14) 41 | 42 | - caption: Упражнения (примерни решения) 43 | chapters: 44 | - file: labs/lab01_solutions 45 | title: Упражнение 1 46 | - file: labs/lab02_solutions 47 | title: Упражнение 2 48 | - file: labs/lab03_solutions 49 | title: Упражнение 3 50 | - file: labs/lab04_solutions 51 | title: Упражнение 4 52 | 53 | - caption: Проекти 54 | chapters: 55 | - file: projects 56 | title: Указания 57 | - file: example_projects 58 | title: Проекти от минали години -------------------------------------------------------------------------------- /labs/lab06/solution-example/tests/actions/test_add.py: -------------------------------------------------------------------------------- 1 | from app.actions.add import add 2 | from app.exceptions.validation import ( 3 | MissingTitleException, 4 | MissingContentException, 5 | NoteAlreadyExistsException, 6 | ) 7 | from app.models.note import Note 8 | 9 | import pytest 10 | 11 | 12 | def test_add_valid_note_no_due_date(): 13 | # arrange 14 | notes = {} 15 | title = "title" 16 | content = "content" 17 | due_date = None 18 | expected = { 19 | title: Note( 20 | title=title, 21 | content=content, 22 | due_date=due_date, 23 | ) 24 | } 25 | 26 | # act 27 | result = add(title, content, due_date, notes) 28 | 29 | # assert 30 | assert result == expected 31 | 32 | def test_add_valid_note_with_due_date(): 33 | # arrange 34 | notes = {} 35 | title = "title" 36 | content = "content" 37 | due_date = "2021-12-31" 38 | expected = { 39 | title: Note( 40 | title=title, 41 | content=content, 42 | due_date=due_date, 43 | ) 44 | } 45 | 46 | # act 47 | result = add(title, content, due_date, notes) 48 | 49 | # assert 50 | assert result == expected 51 | 52 | def test_add_missing_title(): 53 | # arrange 54 | notes = {} 55 | title = "" 56 | content = "content" 57 | due_date = None 58 | 59 | # act & assert 60 | with pytest.raises(MissingTitleException): 61 | add(title, content, due_date, notes) 62 | 63 | def test_add_missing_content(): 64 | # arrange 65 | notes = {} 66 | title = "title" 67 | content = "" 68 | due_date = None 69 | 70 | # act & assert 71 | with pytest.raises(MissingContentException): 72 | add(title, content, due_date, notes) 73 | 74 | def test_add_note_already_exists(): 75 | # arrange 76 | notes = { 77 | "title": Note( 78 | title="title", 79 | content="content", 80 | due_date=None, 81 | ) 82 | } 83 | title = "title" 84 | content = "content2" 85 | due_date = "arbitrary" 86 | 87 | # act & assert 88 | with pytest.raises(NoteAlreadyExistsException): 89 | add(title, content, due_date, notes) -------------------------------------------------------------------------------- /12 - Modules/game/engine.py: -------------------------------------------------------------------------------- 1 | from game.player import Player 2 | 3 | from enum import Enum 4 | import string 5 | 6 | 7 | class GameState(Enum): 8 | ONGOING = 0 9 | WON = 1 10 | LOST = 2 11 | 12 | 13 | class BesenitsaEngine: 14 | def __init__(self, word: str, player: Player): 15 | if len(word) < 3: 16 | raise ValueError("The word must have at least 3 characters!") 17 | 18 | if any(map(lambda c: c.upper() not in string.ascii_uppercase, word)): 19 | raise ValueError("The word must contain only ASCII letters!") 20 | 21 | self.player = player 22 | self.word = word.upper() 23 | self.revealed = {self.word[0], self.word[-1]} 24 | self.__update_masked_word() 25 | 26 | def __update_masked_word(self): 27 | self.masked_word = "".join( 28 | char if char in self.revealed else "_" 29 | for char in self.word 30 | ) 31 | 32 | def guess(self) -> GameState: 33 | attempt = self.player.guess(self.masked_word, self.revealed).upper() 34 | 35 | assert attempt not in self.revealed, "Attempted to guess a char again." 36 | 37 | self.revealed.add(attempt) 38 | self.__update_masked_word() 39 | 40 | if attempt not in self.word: 41 | self.player.take_fail() 42 | if self.player.is_dead: 43 | return GameState.LOST 44 | elif "_" not in self.masked_word: 45 | return GameState.WON 46 | 47 | return GameState.ONGOING 48 | 49 | 50 | if __name__ == "__main__": 51 | # Executed when running `python3 -m game.engine` 52 | 53 | from game.players.mock_player import MockPlayer 54 | 55 | print("Testing win case...") 56 | player_win = MockPlayer(1, "oba") 57 | engine_win = BesenitsaEngine("foobar", player_win) 58 | 59 | assert engine_win.guess() == GameState.ONGOING 60 | assert engine_win.guess() == GameState.ONGOING 61 | assert engine_win.guess() == GameState.WON 62 | print("Test OK.") 63 | 64 | print("Testing lose case...") 65 | player_lose = MockPlayer(3, "asdg") 66 | engine_lose = BesenitsaEngine("foobar", player_lose) 67 | 68 | assert engine_lose.guess() == GameState.ONGOING 69 | assert engine_lose.guess() == GameState.ONGOING 70 | assert engine_lose.guess() == GameState.ONGOING 71 | assert engine_lose.guess() == GameState.LOST 72 | print("Test OK.") 73 | -------------------------------------------------------------------------------- /project-setup-demo/game/engine.py: -------------------------------------------------------------------------------- 1 | from game.player import Player 2 | 3 | from enum import Enum 4 | import string 5 | 6 | 7 | class GameState(Enum): 8 | ONGOING = 0 9 | WON = 1 10 | LOST = 2 11 | 12 | 13 | class BesenitsaEngine: 14 | def __init__(self, word: str, player: Player): 15 | if len(word) < 3: 16 | raise ValueError("The word must have at least 3 characters!") 17 | 18 | if any(map(lambda c: c.upper() not in string.ascii_uppercase, word)): 19 | raise ValueError("The word must contain only ASCII letters!") 20 | 21 | self.player = player 22 | self.word = word.upper() 23 | self.revealed = {self.word[0], self.word[-1]} 24 | self.__update_masked_word() 25 | 26 | def __update_masked_word(self): 27 | self.masked_word = "".join( 28 | char if char in self.revealed else "_" 29 | for char in self.word 30 | ) 31 | 32 | def guess(self) -> GameState: 33 | attempt = self.player.guess(self.masked_word, self.revealed).upper() 34 | 35 | assert attempt not in self.revealed, "Attempted to guess a char again." 36 | 37 | self.revealed.add(attempt) 38 | self.__update_masked_word() 39 | 40 | if attempt not in self.word: 41 | self.player.take_fail() 42 | if self.player.is_dead: 43 | return GameState.LOST 44 | elif "_" not in self.masked_word: 45 | return GameState.WON 46 | 47 | return GameState.ONGOING 48 | 49 | 50 | if __name__ == "__main__": 51 | # Executed when running `python3 -m game.engine` 52 | 53 | from game.players.mock_player import MockPlayer 54 | 55 | print("Testing win case...") 56 | player_win = MockPlayer(1, "oba") 57 | engine_win = BesenitsaEngine("foobar", player_win) 58 | 59 | assert engine_win.guess() == GameState.ONGOING 60 | assert engine_win.guess() == GameState.ONGOING 61 | assert engine_win.guess() == GameState.WON 62 | print("Test OK.") 63 | 64 | print("Testing lose case...") 65 | player_lose = MockPlayer(3, "asdg") 66 | engine_lose = BesenitsaEngine("foobar", player_lose) 67 | 68 | assert engine_lose.guess() == GameState.ONGOING 69 | assert engine_lose.guess() == GameState.ONGOING 70 | assert engine_lose.guess() == GameState.ONGOING 71 | assert engine_lose.guess() == GameState.LOST 72 | print("Test OK.") 73 | -------------------------------------------------------------------------------- /13 - Clean code/example2.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | 3 | def create_meme(image_path, top_text, bottom_text, output_path, font_path="arial.ttf", font_size=40, resize_width=None): 4 | # Open the image 5 | img = Image.open(image_path) 6 | 7 | # Resize image if requested 8 | if resize_width: 9 | aspect_ratio = img.height / img.width 10 | new_height = int(resize_width * aspect_ratio) 11 | img = img.resize((resize_width, new_height), Image.ANTIALIAS) 12 | 13 | draw = ImageDraw.Draw(img) 14 | width, height = img.size 15 | 16 | # Load the custom font 17 | try: 18 | font = ImageFont.truetype(font_path, size=font_size) 19 | except IOError: 20 | print("Defaulting to a basic font. Install a TTF font for better styling.") 21 | font = ImageFont.load_default() 22 | 23 | # Define text position and styling 24 | margin = 10 25 | 26 | # Add top text 27 | if top_text: 28 | top_text_size = draw.textsize(top_text, font=font) 29 | top_position = ((width - top_text_size[0]) // 2, margin) 30 | draw.text(top_position, top_text, fill="white", font=font, stroke_fill="black", stroke_width=2) 31 | 32 | # Add bottom text 33 | if bottom_text: 34 | bottom_text_size = draw.textsize(bottom_text, font=font) 35 | bottom_position = ((width - bottom_text_size[0]) // 2, height - bottom_text_size[1] - margin) 36 | draw.text(bottom_position, bottom_text, fill="white", font=font, stroke_fill="black", stroke_width=2) 37 | 38 | # Save the result 39 | img.save(output_path) 40 | print(f"Meme saved at {output_path}") 41 | 42 | # Example usage 43 | if __name__ == "__main__": 44 | # Get user inputs 45 | image_path = input("Enter the path to the image: ") 46 | top_text = input("Enter the top text (leave blank for none): ") 47 | bottom_text = input("Enter the bottom text (leave blank for none): ") 48 | output_path = input("Enter the output path (e.g., output.jpg): ") 49 | 50 | # Optional customization 51 | font_path = input("Enter the font path (leave blank for default Arial): ") or "arial.ttf" 52 | font_size = int(input("Enter the font size (default 40): ") or 40) 53 | resize_width = input("Enter the resize width (leave blank for no resizing): ") 54 | resize_width = int(resize_width) if resize_width else None 55 | 56 | # Generate the meme 57 | create_meme(image_path, top_text, bottom_text, output_path, font_path, font_size, resize_width) 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Jetbrains IDEs folders 132 | .idea/ 133 | .fleet/ 134 | 135 | # macOS 136 | .DS_Store 137 | 138 | # VSCode 139 | .vscode -------------------------------------------------------------------------------- /console-demo/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | A console application that shows the current weather forecast for a given location. 5 | It accepts as arguments: 6 | - latitude (--lat) (required) 7 | - longitude (--lon) (required) 8 | - a flag indicated whether to print the temperatures predicted for the whole day instead (-d) 9 | 10 | Example usage: 11 | 12 | $ python3 app.py --lat 42 --lon 23 13 | $ python3 app.py --lat 42 --lon 23 -d 14 | $ python3 app.py --help 15 | """ 16 | 17 | import requests # you need to install it via `pip install requests` 18 | import argparse # built-in, no installation needed 19 | import dotenv # you need to install it via `pip install python-dotenv` 20 | 21 | import sys 22 | import os 23 | 24 | dotenv.load_dotenv() # exports contents of a `.env` file in the curr dir as environmental variables 25 | API_KEY = os.getenv("API_KEY") 26 | 27 | if not API_KEY: 28 | error_msg = """ 29 | Please supply a valid API key using either 30 | `export API_KEY=...` before running the command 31 | or 32 | creating a file named `.env` in the current directory and putting API_KEY=... inside. 33 | """ 34 | 35 | # When writing a console app 36 | # it is better to write error messages to STDERR instead of STDOUT 37 | # (the `print` function will write them to STDOUT) 38 | sys.stderr.write(error_msg) 39 | exit(1) 40 | 41 | parser = argparse.ArgumentParser(description="Get the weather for a given location") 42 | parser.add_argument("--lat", type=float, required=True, help="latitude of the location") 43 | parser.add_argument("--lon", type=float, required=True, help="longitude of the location") 44 | parser.add_argument("-d", "--day", action="store_true", help="whether to print the temperatures predicted for the whole day instead") 45 | args = parser.parse_args() 46 | 47 | lat = args.lat 48 | lon = args.lon 49 | is_requesting_whole_day = args.day 50 | 51 | response = requests.get( 52 | "https://api.openweathermap.org/data/2.5/forecast", 53 | params={ 54 | "lat": lat, 55 | "lon": lon, 56 | "appid": API_KEY, 57 | "units": "metric", 58 | } 59 | ) 60 | 61 | if not response: 62 | sys.stderr.write(f"Error fetching data ({response.status_code=}). Please try again later.\n") 63 | exit(1) 64 | 65 | forecasts = response.json()["list"] 66 | forecasts.sort(key=lambda x: -x["dt"]) # biggest (most recent) datetime first 67 | 68 | if not is_requesting_whole_day: 69 | temperature = forecasts[0]["main"]["temp"] 70 | print(temperature) 71 | else: 72 | temps = "\n".join( 73 | str(forecast["main"]["temp"]) 74 | for forecast in forecasts[:8] 75 | ) 76 | print(temps) 77 | -------------------------------------------------------------------------------- /04 - Functional Programming/tasks/tasks.md: -------------------------------------------------------------------------------- 1 | # Задача 1 2 | Напишете функция, която приема двумерен списък (матрица) и индекс на колона. 3 | Функцията трябва да връща всички елементи в колоната 4 | 5 | # Задача 2 6 | Нека е даден абстрактен клас `ArithmeticOperation`, който има метод `apply`. 7 | Той приема като аргумент два списъка и прилага дадена аритметична операция върху тях. 8 | 9 | Трябва да се дефинират четири наследника на `ArithmeticOperation`: 10 | - `Add`, който събира почленно елементите на двата списъка 11 | - `Subtract`, който изважда почленно елементите на двата списъка 12 | - `Multiply`, който умножава почленно елементите на двата списъка 13 | - `Divide`, който дели почленно елементите на двата списъка 14 | 15 | Ако единия списък съдържа повече елементи, те участват в резултата, без прилагането на каквато и да е операция върху тях. 16 | т.е. `Multiply([1, 2, 3], [4, 5, 6, 7])` следва да върне `[4, 10, 18, 7]` 17 | 18 | Да се дефинира функция `apply_arithmetic_operations`, която приема два аргумента: 19 | поредица от действия, които да бъдат извършени, и елементите върху които да бъдат извършени. 20 | 21 | Редът на изпълнение и аргументите върху които се изпълняват операциите е следния: 22 | Нека имаме елементите `[1], [2], [4]` и операциите `[Add(), Subtract()]` 23 | 1. Взима се първия и втория елемент (`[1]` и `[2]`) 24 | 2. Взима се първата операция (`Add()`) 25 | 3. Изпълнява се съответната операция. Резултатът и е `[3]` 26 | 4. Резултатът от предишната операция се взима като първи аргумент. Втория аргумент е следващия елемент от списъка с елементи (`[4]`). 27 | 5. Взима се следватаща операция (`Subtract()`) 28 | 6. Изпълнява се съответната операция - тук резултатът e `[-1]` 29 | 30 | Този процес продължава до изчерпване на операциите/елементите. 31 | В края се връща резултата от последната изпълнена операция. 32 | 33 | ### Пример 1: 34 | ```python 35 | actions = [Add(), Add(), Multiply()] 36 | items = [[1, 2, 3], [4, 5, 6, 7], [10, 10, 10, 10], [1, -1, 1, -1]] 37 | 38 | print(apply_arithmetic_operations(actions, items)) 39 | ``` 40 | `[15, -17, 19, -17]` 41 | 42 | ``` 43 | Add([1, 2, 3], [4, 5, 6, 7]) => [5, 7, 9, 7] 44 | Add([5, 7, 9, 7], [10, 10, 10, 10]) => [15, 17, 19, 17] 45 | Multiply([15, 17, 19, 17], [1, -1, 1, -1]) => [15, -17, 19, -17] 46 | ``` 47 | 48 | 49 | ### Пример 2: 50 | ```python 51 | actions = [Multiply(), Divide(), Multiply()] 52 | items = [[1, 3, 5], [2, 2, 2], [3, 3, 3], [10, 10]] 53 | 54 | print(apply_arithmetic_operations(actions, items)) 55 | ``` 56 | `[6.666666666666666, 20.0, 3.3333333333333335]` 57 | 58 | ``` 59 | Multiply([1, 3, 5], [2, 2, 2]) => [2, 6, 10] 60 | Divide([2, 6, 10], [3, 3, 3]) => [0.666666666666666, 2, 3.3333333333333335] 61 | Multiply([0.666666666666666, 2, 3.3333333333333335], [10, 10]) => [6.666666666666666, 20.0, 3.3333333333333335] 62 | ``` 63 | 64 | ### Пример 3: 65 | ```python 66 | actions = [Add(), Subtract(), Divide()] 67 | items = [[1, 3, 5], [2, 4, 6], [2, 4, 6], [1, 1, 1]] 68 | 69 | print(apply_arithmetic_operations(actions, items)) 70 | ``` 71 | `[1.0, 3.0, 5.0]` 72 | 73 | ``` 74 | Add([1, 3, 5], [2, 4, 6]) => [3, 7, 11] 75 | Subtract([3, 7, 11], [2, 4, 6]) => [1, 3, 5] 76 | Divide([1, 3, 5], [1, 1, 1]) => [1.0, 3.0, 5.0] 77 | ``` 78 | -------------------------------------------------------------------------------- /projects.md: -------------------------------------------------------------------------------- 1 | # Указания към проектите за курса "Програмиране с Python" 2 | 3 | ## Какво е проект ? 4 | 5 | ​ Въпреки че темите за проекти са отворени, можем да ви дадем няколко идеи какво очакваме. Ще разделим проектите на няколко типа, и ще дадем общи насоки за всяка от тях: 6 | 7 | #### Общи насоки: 8 | 9 | - Очакваме различни функционалности, в зависимост от темата. Примерно, ако правите приложение за бележки през конзолата, очакваме да имате начин за запазване на бележки, категоризиране, напомняния и т.н. 10 | - Проектите ви трябва да могат да бъдат пуснати на друга машина - кратък INSTALL.md или инструкции в README.md биха ни свършили работа 11 | - Може да използвате всякакви библиотеки, но очакваме да имате достатъчно функционалност написана от вас 12 | - Трябва да имате тестове 13 | - Трябва проекта да е качен в Git (Github, Gitlab, Bitbucket, т.н.) 14 | - Добър error handling е очакван 15 | - Очакваме демонстрация на проекта - 5-10 минути демонстрация и 5 минути за въпроси 16 | 17 | ## Примерни идеи и насоки към тях 18 | 19 | 1. ### Уеб приложение 20 | 21 | - Може да използвате Flask, Django, или някакъв друг web framework 22 | - Ще се оценява частта написана на Python (т.е. ако използвате Angular/JS/TS за front-end-а, а backend-а е на Python, ще оценяваме само Python частта) 23 | - Може да напишете и само backend/API, но очакваме да може да демонстрирате работата му по някакъв начин 24 | 25 | 2. ### Конзолно приложение 26 | 27 | - The sky is the limit here - може да направите много неща, в зависимост от вашите интереси 28 | - Очакваме да имате адекватен CLI 29 | - Четене/писането от файл е препоръчителна функционалност 30 | 31 | 3. ### Desktop приложение 32 | 33 | - Въпреки, че в рамките на курса няма да успеем да разгледаме примери за GUI framework, остава възможността някой да направи desktop приложение с GUI 34 | - Може да използвате PySimpleGui, TKInter, PyQT или друг framework 35 | - Няма да оценяваме красота на интерфейса, но ще оценяваме начина по който е написан 36 | 37 | 4. ### Игри 38 | 39 | - Както и при desktop приложенията, въпреки че не сме разглеждали framework за игри, все пак даваме възможност за създаване на игра 40 | - Може да използвате PyGame или други 41 | - Може и да пропуснете графичния интерфейс, и да направите игра, която да се играе в конзолата (текстов интерфейс е валиден вариант), но тогава очакваме малко повече функционалности 42 | 43 | 5. ### Друго 44 | 45 | - Ако проекта ви не се вписва в никоя от горните 4 категории - поздравления, ще правите нещо много интересно 46 | - Ако ще ни показвате някакъв проект свързан с AI/ML - ще се изкефим, но очакваме нещо повече от невронна мрежа с `pytorch`, `keras` или `tensorflow` 47 | - Ако имате идея да напишете библиотека за нещо което го няма - очакваме много добро пакетиране, разделение на модули, тестове, добри абстракции и error handling 48 | - Wrap-ването на някаква C/C++ библиотека в Python също би било интересен проект, но ще очакваме малко повече от един wrapper 49 | 50 | ## Как да си избера тема проект ? 51 | 52 | 1. Измисляте си тема 53 | 2. Записвате я в канала `projects` в Дискорд 54 | 3. Обсъждате я с нас 55 | 4. Одобряваме я или я пращаме за rework 56 | 57 | ​ **Крайният срок за избирането на тема е края на семестъра - 19.01.2023**. **Крайният срок за проектите е 11.02.2023** 58 | 59 | ## Какви са критериите за проект ? 60 | 61 | 62 | **Total**: 40 точки 63 | 64 | - **Защита** - 10 точки 65 | - **Функционалност** - 10 точки 66 | - **Разделение на модули** - 3 точки 67 | - **Използване на "Питонизми"** - 3 точки 68 | - list comprehensions, generators, any/all, slicing 69 | 70 | - **Използване на type hints** - 3 точки 71 | - Автоматично оценено, чрез `mypy` 72 | - **PEP-8** - 3 точки 73 | - Автоматично оценено, чрез `pylint` 74 | - **Тестове** - 5 точки 75 | - Автоматично оценено, чрез `coverage` 76 | - **requirements.txt** - 1 точка 77 | - Автоматично оценено 78 | 79 | - **Git** - 1 точки 80 | - **README** - 1 точка 81 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/main.py: -------------------------------------------------------------------------------- 1 | """Entry point script of the Note-Taking Application. Intended to be run from the command line.""" 2 | 3 | import argparse 4 | import sys 5 | 6 | from app.actions.add import add 7 | from app.actions.delete import delete 8 | from app.actions.edit import edit 9 | from app.exceptions.validation import ( 10 | ValidationException, 11 | InvalidActionException, 12 | MissingTitleException, 13 | MissingContentException, 14 | ) 15 | from app.models.note import Note 16 | from app.storage.json_file import JsonFileStorage 17 | from app.ui.note_details import note_details 18 | from app.ui.notes_list import list_all_notes 19 | from app.ui.success_messages import ( 20 | ADD_SUCCESS_MESSAGE, 21 | DELETE_SUCCESS_MESSAGE, 22 | EDIT_SUCCESS_MESSAGE, 23 | ) 24 | 25 | 26 | JSON_FILE = "notes.json" # Path to the JSON file where notes are stored 27 | 28 | 29 | def _parse_cli_args() -> argparse.Namespace: 30 | parser = argparse.ArgumentParser(description="Command-line Note-Taking Application") 31 | parser.add_argument("action", choices=["add", "view", "delete", "edit", "list"], 32 | help="What do you want to do?") 33 | parser.add_argument("--title", type=str, 34 | help="Title of the note (required for all actions without `list`)") 35 | parser.add_argument("--content", type=str, 36 | help="Content of the note (required for `add`, optional for `edit`)") 37 | parser.add_argument("--due-date", type=str, 38 | help="Optional due date (for `add` and `edit` actions)") 39 | args = parser.parse_args() 40 | return args 41 | 42 | def _require_title(args: argparse.Namespace) -> str: 43 | title = args.title 44 | if title is None: 45 | raise MissingTitleException 46 | return title 47 | 48 | def _require_content(args: argparse.Namespace) -> str: 49 | content = args.content 50 | if content is None: 51 | raise MissingContentException 52 | return content 53 | 54 | 55 | def action_output( 56 | args: argparse.Namespace, 57 | notes: dict[str, Note], 58 | ) -> tuple[str, dict[str, Note]]: 59 | """Return a tuple containing the output string and the updated notes dictionary.""" 60 | 61 | match args.action: 62 | case "add": 63 | title = _require_title(args) 64 | content = _require_content(args) 65 | notes = add(title, content, args.due_date, notes) 66 | output = ADD_SUCCESS_MESSAGE.format(title=title) 67 | case "edit": 68 | title = _require_title(args) 69 | notes = edit(title, args.content, args.due_date, notes) 70 | output = EDIT_SUCCESS_MESSAGE 71 | case "delete": 72 | title = _require_title(args) 73 | notes = delete(title, notes) 74 | output = DELETE_SUCCESS_MESSAGE 75 | case "view": 76 | title = _require_title(args) 77 | note = notes[title] 78 | output = note_details(note) 79 | case "list": 80 | notes_list = list(notes.values()) 81 | output = list_all_notes(notes_list) 82 | case _: 83 | raise InvalidActionException 84 | 85 | return output, notes 86 | 87 | 88 | def main(): 89 | """Main entry point of the application.""" 90 | 91 | args = _parse_cli_args() 92 | storage = JsonFileStorage(JSON_FILE) 93 | 94 | notes = storage.load_all_notes() 95 | output, notes = action_output(args, notes) 96 | storage.save_all_notes(notes) 97 | 98 | print(output) 99 | 100 | 101 | if __name__ == "__main__": 102 | try: 103 | main() 104 | except ValidationException as ve: 105 | print(ve, file=sys.stderr) # хубаво е всички грешки да се пренасочват към STDERR 106 | # (`print` defaults to STDOUT) 107 | sys.exit(1) # exit with a non-zero status code to indicate an error 108 | -------------------------------------------------------------------------------- /01 - Intro to Python/notebooks.md: -------------------------------------------------------------------------------- 1 | # Как да си пускаме лекциите и материалите от курса? 2 | 3 | Всички лекции са под формата на Jupyter notebooks. Това са "тетрадки", чието съдържание е структурирано под формата на клетки, които биват два типа - такива с текст (в [Markdown формат](https://www.markdownguide.org/basic-syntax/)) и такива с код. 4 | 5 | Клетките с код са изпълними и техният резултат от изпълнението е изписан непосредствено под тях. 6 | 7 | В случай, че просто искате да четете лекциите, без да редактирате/изпълнявате съдържанието, можете да ги отваряте в [сайта на курса](https://fmipython.github.io/PythonCourse2024/README.html) (който е под формата на [Jupyterbook](https://jupyterbook.org/en/stable/intro.html)) или директно в GitHub да ги разглеждате. 8 | 9 | Ако желаете да си експериментирате с примерите, които се дават в лекциите, и да им подкарвате кода, то има няколко начина за това: 10 | 11 | ## Вариант 1 (най-лесен): Онлайн, чрез Google Collab бутона в Jupyterbook-a 12 | 13 | 1. Отворете съответната лекция в [сайта на курса](https://fmipython.github.io/PythonCourse2024/README.html) 14 | 2. Най-горе има една иконка на ракетка, като при натискането ѝ има опцията да се отвори лекцията в Google Collab: 15 | 16 | ![Collab light button](assets/collab-light.png) 17 | 18 | Това ще създаде копие на notebook-a във вашия акаунт в Google Collab, с цел да можете колкото си искате да експериментирате. Кодът се изпълнява в клауд (на машини на Google). 19 | 20 | 🚀 За желаещите да се заниимават професионално с Data Science / AI / ML е препоръчително да си поиграят там, понеже Google Collab се използва изключително широко в тази практика и е хубаво човек да свикне със средата на разработка, която платформата предоставя. 21 | 22 | ## Вариант 2: Локално, чрез JupyterLab 23 | 24 | 1. Изтеглете от гитхъб репото на курса 25 | ```bash 26 | git clone https://github.com/fmipython/PythonCourse2024.git 27 | ``` 28 | 29 | 2. Навигирайте до папката на курса 30 | ```bash 31 | cd PythonCourse2024 32 | ``` 33 | 34 | 3. Изтеглете Jupyter и JupyterLab 35 | ```bash 36 | pip install jupyter jupyterlab 37 | ``` 38 | (възможно е да трябва да напишете `pip3` вместо `pip`) 39 | 40 | 4. Изпълнете командата: 41 | ```bash 42 | jupyter lab 43 | ``` 44 | 45 | ![Jupyter](assets/jupyter.png) 46 | 47 | ## Вариант 3 (arguably най-удобен): Директно във Visual Studio Code 48 | 49 | 1. Изтеглете от GitHub репото на курса (ако го нямате вече) 50 | 51 | Това може да стане по различни начини, като например: 52 | * **през терминала**: 53 | 1. Командата, която да изпълните (в директорията, в която искате да се създаде и изтегли папката на курса) е: 54 | ```bash 55 | git clone https://github.com/fmipython/PythonCourse2024.git 56 | ``` 57 | 2. След което отворете във VSCode папката на курса. Това може да стане или чрез графичния интерфейс, или (ако сте добавили VSCode във PATH) чрез командата `code PythonCourse2024`. 58 | 59 | * **през VSCode в браузъра**: 60 | 1. 🪄 В [Github страницата на курса](https://github.com/fmipython/PythonCourse2024) (и по принцип във всяко Github repository) можете да натиснете `.` (точка) на клавиатурата, за да отворите Web VSCode ✨ 61 | 2. 😕 В него ще можете да гледате и редактирате съдържанията на файловете, но няма да можете да изпълнявате клетките (понеже това изисква IPython kernel, който трябва да бъде пуснат, което може да стане или в Github Codespace, или локално на вашата машина) (а Codespace-ът е общ за всички и струва пари, so...). 62 | 3. 💻 Затова оттук ще трябва кликнете "Github" бутона най-долу най-вляво и след това да изберете опцията Continue working in a new local clone: 63 | 64 | ![Github](assets/github.png) 65 | 66 | ![New local clone](assets/new-local-clone.png) 67 | 68 | 2. Свалете Jupyter (ако го нямате вече) 69 | 70 | Отворете терминал (може и вградения такъв във VSCode чрез View > Terminal) и напишете: 71 | ```bash 72 | pip3 install jupyter ipykernel 73 | ``` 74 | (възможно е да трябва да напишете `pip` вместо `pip3`, най-често ако сте на Windows) 75 | 76 | 3. Свалете [Jupyter extension-а за VSCode](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) (ако го нямате вече) 77 | 78 | 4. Отворете някоя лекция във VSCode. 79 | 80 | Би трябвало да изглежда по подобен начин: 81 | 82 | ![VSCode Jupyter Notebooks](assets/vscode-jupyter.png) 83 | 84 | *(скрийншотът е от предишна година but you get the point, не ми се качва нов)* 85 | 86 | Бутонът горе вдясно първият път ще е с текст "Select Kernel". От него избирате конкретният Python интерпретатор, с който да се изпълняват клетките. 87 | 88 | Чрез Run бутоните (или Ctrl+Enter) можете да изпълнявате клетките. 89 | -------------------------------------------------------------------------------- /labs/lab06/solution-example/README.md: -------------------------------------------------------------------------------- 1 | # Примерно решение на задачата 2 | 3 | Няма един правилен начин, но определено могат да се дадат аргументи за това защо даден подход може да е по-предпочитан от друг. 4 | 5 | Тук ви представяме как ние бихме написали приложението. 6 | 7 | Една лека забележка, че за такова малко апп-че някои неща и на нас може да ни се струват overengineering, но обосновката ни е, че 8 | 9 | 1. Целта е да се упражняваме и да се научим на добрите практики 10 | 2. Всеки проект, по който се работи, расте. Aко от самото начало се придържаме към добрите практики, ще е по-лесно да поддържаме и разширяваме проекта по-късно. Този конкретен много лесно може да избухне в unmaintanable код, ако не се погрижим за него и ако се променят постоянно изискванията и се добавят нови. Познайте на работа обикновено дали не става точно така 🙃 11 | 12 | 13 | ## Обосновка за отделните пакети 14 | 15 | *(пише ги и в `__init__.py`-четата им)* 16 | 17 | 1. `actions`: 18 | 19 | Пакетът съдържа модули, които имплементират бизнес/core логиката на приложението, 20 | т.е. какво може да прави, как се очаква да се държи при еди-си-какви-си обстоятелства. 21 | 22 | В нашия случай решихме да направим функциите "чисти" (pure) - такива без странични ефекти, 23 | защото: 24 | 1. да бъдат тествани по-лесно (няма нужда са се мокват файлове и принтове) 25 | 2. да не зависят от имплементационни детайли (JSON file-ове, print-ове, т.н.) 26 | 3. single responsibility 27 | 28 | В т.нар. clean architecture, такива класове/модули/функции могат обикновено се наричат Use Cases. 29 | 30 | 2. `exceptions`: 31 | 32 | Пакетът дефинири наши грешки, които за удобство да използваме в модулите с бизнес логика, така че да не зависят от print-ове и конкретни стрингове. 33 | 34 | 3. `models`: 35 | 36 | Т.нар. "модел" е общоприет термин в много architectural & design patterns, 37 | който обозначава клас (тип проста структура от данни), 38 | който репрезентира вярно и пълно цялата информация, с която борави приложението. 39 | 40 | В нашия случай, това е клас `Note`, който представя една бележка в нашето приложение. 41 | 42 | Причини: 43 | 44 | 1. По-лесна и необъркваща е работата с такива класове. 45 | 2. По-лесно е да се добавят нови полета и функционалности към класа, отколкото да се копи-пействат ключовете на речник, индексите на tuple, т.н. 46 | 47 | Нашите модели (тези, които репрезентират core данните и логиката ни), 48 | се наричат **домейн модели**. Можем да имаме Database модели, Network модели и т.н. 49 | 50 | Моделите на приложението не е хубаво да зависят от конкретните имлоементационни детайли, 51 | напр. на базата данни, 52 | на формата на файлове (JSON, CSV, XML, YAML, ...), 53 | на външни API-та и т.н. 54 | 55 | Най-удачно е, ако ползваме например външно API, което предоставя обекти с малко по-различна структура от нашата, 56 | то да създадем отделни network модели, които са валидни само за API-то, 57 | и да конвертираме между тях и нашите домейн модели. 58 | 59 | 4. `storage`: 60 | 61 | Пакет, съдържащ всички начини на съхранение на бележки. 62 | 63 | В момента има само един такъв: чрез JSON файл. 64 | 65 | В бъдеще могат да лесно бъдат добавени и други, като база данни и т.н. 66 | В такъв случай ще е удачно да бъде отделен общият им интерфейс в абстрактен базов клас (ABC), 67 | така че другите компоненти на приложението (в случая само `main`) да могат да съхраняват данните, 68 | без да знаят конкретните детайли на реализацията (**Dependency Inversion** принципът). 69 | 70 | 5. `ui`: 71 | 72 | Този пакет съдържа компонентите на потребителския интерфейс на приложението. 73 | 74 | Тъй като това е просто CLI приложение, 75 | потребителският интерфейс тук е реализиран с помощта на низове, които да бъдат принтирани. 76 | 77 | Тук може да бъде мястото за реализиране на по-сложни потребителски интерфейси, 78 | като GUI (например с `tkinter` / `wxpython`, `pygame`) или някакъв уеб интерфейс и т.н. 79 | 80 | 81 | ## Връзки и зависимости 82 | 83 | 84 | * `models` и `exceptions`: 85 | * не зависят от нищо 86 | * чисти структури от данни (в случая общи за цялото приложение) 87 | 88 | * `actions`: 89 | * зависи от `models` и `exceptions` 90 | * използва се само от `main`. 91 | 92 | * `storage`: 93 | * зависи от `models` (евентуално и от `exceptions`, ако добавим подходящи грешки) 94 | * използва се само от `main` 95 | 96 | * `ui`: 97 | * зависи от `models` (евентуално и от `exceptions`, но в слуачая сме дефинирали съобщенията за грешки в самите Exception класове) 98 | * използва се само от `main` 99 | 100 | * `main.py` е спойката между отделните модули. Създава конкретни имплементации, извиква конкретни функции, и ги свързва. В случая на нашия CLI app също чете входа и пише изхода. 101 | -------------------------------------------------------------------------------- /labs/lab05/lab05.md: -------------------------------------------------------------------------------- 1 | # Упражнение №5 2 | 3 | ## Накратко за задачата 4 | 5 | Направете конзолно приложение, което да извежда trending филми и сериали, взимайки информацията от [The Movie Database API](https://developer.themoviedb.org/reference/intro/getting-started). 6 | 7 | ## Условие 8 | 9 | В текущата директория се намира файлът `main.py`, който трябва да бъде направен да може да получава аргументи от командния ред, и при изпълнение да извежда на стандартния изход резултата от изпълнението. Точните изисквания за аргументите и изхода са описани по-долу. 10 | 11 | **Добавяйте каквито пакети и модули сметете за необходимо към текущата директория - това е основният фокус на упражнението.** 12 | 13 | Работете във virtual envronment и създайте requirements.txt файл с използваните точни версии на външните библиоетки. 14 | 15 | #### Аргументи 16 | 17 | Аргументите на програмата ще са точно 3, които трябва да се подават от командния ред. 18 | 19 | За взимане на подадените аргументи от командния ред, може да ползвате `sys.argv` от вградената библиотека `sys`. Това е лист от `str`-ове, като първият елемент е името на изпълнимия файл, а останалите са аргументите, подадени от командния ред, които в тази задача трябва да бъдат (в следния указан ред): 20 | 21 | 1. `tv`, `movies` или `all` (дали ще показваме само trending сериали, или филми, или без значение от вида) 22 | 2. `day` или `week` (дали ще показваме trending за деня или за седмицата) 23 | 3. `csv` или `json` (дали ще извеждаме резултата в CSV или JSON формат) 24 | 25 | *В идеалния случай трябва да има валидация на аргументите, но това не е задължително за получаване на точките за функционалност, поради обема на задачата.* 26 | 27 | #### API 28 | 29 | 1. API read access token **ще ви пратим в дискорд** (другият вариант е да минете сами през [процедурата за регистрация и създаване на ваш api key](https://www.themoviedb.org/signup)). Той трябва да бъде подаден във всеки request към API-то в header-а `Authorization` със стойност `Bearer ...` (където `...` е токенът). 30 | 2. Ползвайте GET trending ендпойнтите ([за "tv"](https://developer.themoviedb.org/reference/trending-tv) и [за "movie"](https://developer.themoviedb.org/reference/trending-movies)). Предайте "day" или "week" в заявките, спрямо това каква е стойността на втория аргумент. Не заявявайте повече от 1 page в request-ите. 31 | 3. Сайтът им освен да документира добре какво се очаква от request-ите и response-ите, също така и е доста интерактивен, позволяйки да си тествате на място заявките. 32 | 33 | #### Изход 34 | 35 | 1. Изходът на програмата да съдържа за всеки филм/сериал единствено: 36 | 1. `title` (str): неговото име 37 | 2. `rating` (float): неговата средна потребителска оценка 38 | 2. Изходът да бъде **сортиран по `rating` в низходящ ред**. 39 | 3. Изходът да бъде във формат [CSV](https://en.wikipedia.org/wiki/Comma-separated_values) или JSON, спрямо това каква е стойността на третия аргумент. 40 | * За конвертиране на `dict` или `list` до JSON `str`, можете да ползвате `json.dumps(...)` от вградената библиотека `json`. 41 | 4. Примерни изходи и в двата формата може да разгледате в `example_output/`. 42 | 43 | ## Асинхронност 44 | 45 | Една точка ще дадем, ако в случая на `all` двете заявки (GET movie & GET TV) се изпълняват едновременно, вместо да се изчакват една друга. Препоръчваме за целта да разучите [aiohttp](https://docs.aiohttp.org/en/stable/). 46 | 47 | 48 | ## Примери 49 | 50 | Команда: 51 | ```bash 52 | python main.py tv day csv 53 | ``` 54 | 55 | Изход: 56 | ```csv 57 | title,rating 58 | South Sea Tomb,8.8 59 | One Piece,8.728 60 | World War II: From the Frontlines,8.7 61 | Attack on Titan,8.668 62 | Jujutsu Kaisen,8.568 63 | My Demon,8.567 64 | The Walking Dead: Daryl Dixon,8.451 65 | Game of Thrones,8.442 66 | High Tides,8.4 67 | Sweet Home,8.396 68 | Fargo,8.305 69 | Monarch: Legacy of Monsters,8.198 70 | Loki,8.184 71 | Blood Coast,7.905 72 | Doctor Who,7.9 73 | Slow Horses,7.79 74 | Doctor Who,7.465 75 | My Life with the Walter Boys,6.9 76 | Obliterated,6.55 77 | Squid Game: The Challenge,6.25 78 | ``` 79 | 80 | Команда: 81 | ```bash 82 | python main.py movie week json 83 | ``` 84 | 85 | Изход: 86 | ```json 87 | [{"title": "Oppenheimer", "rating": 8.14}, {"title": "Five Nights at Freddy's", "rating": 7.844}, {"title": "Killers of the Flower Moon", "rating": 7.716}, {"title": "Mission: Impossible - Dead Reckoning Part One", "rating": 7.592}, {"title": "Leo", "rating": 7.533}, {"title": "The Hunger Games: The Ballad of Songbirds & Snakes", "rating": 7.295}, {"title": "Trolls Band Together", "rating": 7.2}, {"title": "Barbie", "rating": 7.179}, {"title": "The Creator", "rating": 7.131}, {"title": "Wonka", "rating": 7.0}, {"title": "Leave the World Behind", "rating": 6.935}, {"title": "Indiana Jones and the Dial of Destiny", "rating": 6.678}, {"title": "The Killer", "rating": 6.655}, {"title": "Wish", "rating": 6.625}, {"title": "May December", "rating": 6.613}, {"title": "The Marvels", "rating": 6.555}, {"title": "Freelance", "rating": 6.434}, {"title": "Napoleon", "rating": 6.433}, {"title": "Family Switch", "rating": 6.381}, {"title": "Candy Cane Lane", "rating": 6.339}] 88 | ``` 89 | 90 | ## Оценяване 91 | 92 | * 2 т. - за смислено разделяне на кода в допълнителни модули и пакети 93 | * 1 т. - за коректна и пълна функционалност 94 | * 1 т. - за коректен requirements.txt и използване на virtual environment 95 | * 1 т. - за едновременно изпълнение на заявките при `all` 96 | -------------------------------------------------------------------------------- /labs/lab06/README.md: -------------------------------------------------------------------------------- 1 | # Упражнение №6 2 | 3 | В това упражнение ще оправим бъгче, добавим нова функционалност, рефакторираме код и напишем тестове. Почти като на работа 🙃 4 | 5 | ## Съществуващ код 6 | 7 | В папка `spaghetti-notes` се намира Python конзолно приложение - бележник. Използва се по един от следните 4 начина: 8 | 9 | 1. Добавяне на бележка 10 | 11 | ```bash 12 | python main.py add --title "Заглавие на бележката" --content "Съдържание на бележката" 13 | ``` 14 | 15 | Възможно е и да се добави срок към бележката (задачата) чрез `--due-date`. 16 | 17 | 2. Преглед на бележка 18 | 19 | ```bash 20 | python main.py view --title "Заглавие на бележката" 21 | ``` 22 | 23 | 3. Изтриване на бележка 24 | 25 | ```bash 26 | python main.py delete --title "Заглавие на бележката" 27 | ``` 28 | 29 | 4. Преглед на всички бележки 30 | 31 | ```bash 32 | python main.py list 33 | ``` 34 | 35 | ## Задача 36 | 37 | Имате няколко цели, които да изпълните, като **няма значение в какъв ред ще ги изпълните**. Препоръчваме все пак първо да оправите бъга (Цел А), за да се запознаете с кода (и проблемите му) 38 | 39 | ### Цел А: Оправяне на бъг 40 | 41 | Получен е следният бъг от тестери (QA) на софтуера: 42 | 43 | * Bug description: 44 | 45 | After creating a note without a due date, no notes are listed when displaying all notes via `list`. Additionally, when viewing the note with the `view` subcommand, no due date info is displayed. 46 | 47 | * Steps to reproduce: 48 | 49 | 1. `python main.py add --title "Бележка без срок" --content "Съдържание на бележката"` 50 | 2. `python main.py list` 51 | * Expected output: 52 | ``` 53 | Listing notes... 54 | - Бележка без срок (Due: None) 55 | ``` 56 | * Actual output: 57 | ``` 58 | Listing notes... 59 | ``` 60 | 3. `python main.py view --title "Бележка без срок"` 61 | * Expected output: 62 | ``` 63 | Бележка без срок 64 | --- 65 | Съдържание на бележката 66 | --- 67 | No due date. 68 | ``` 69 | * Actual output: 70 | ``` 71 | Бележка без срок 72 | --- 73 | Съдържание на бележката 74 | --- 75 | ``` 76 | 77 | ### Цел Б: Добавяне на нова функционалност 78 | 79 | Бизнес анализаторите ни са добавили нов таск: 80 | 81 | * Description: 82 | 83 | As a user, I want to be able to edit the content and/or due date of an existing note. This needs to be done through a new command `edit`. 84 | 85 | * Acceptance criteria: 86 | 87 | 1. The user should be able to edit the *content* and/or the *due date* of an existing note. 88 | 2. The user should **not** be able to edit the *title* of an existing note. 89 | 4. Upon successful edit, the user should see a message **"Note successfully edited."** 90 | 5. If no note exists with the provided title, the user should see a message **"Note with title {title} does not exist."** 91 | 6. If the user tries to edit a note without providing any new content or due date, the user should see a message **"No content or due date provided - no changes made to the note."** 92 | 7. If the user wished to erase the due date of a note, they should be able to do so by providing `none` as the value of the `--due-date` argument. 93 | 94 | * Example usage: 95 | 96 | ```bash 97 | python main.py edit --title "Some note title" --content "New content" 98 | # 99 | python main.py edit --title "Some note title" --due-date "2026-01-01" 100 | # 101 | python main.py edit --title "Some note title" --due-date none 102 | # 103 | python main.py edit --title "Some note title" --content "New content" --due-date "2026-01-01" 104 | # 105 | python main.py edit --title "Some note title" --content "New content" --due-date none 106 | ``` 107 | 108 | ### Цел В: Рефакториране на кода 109 | 110 | Това изискване няма как да дойде от бизнес анализаторите или QA-те във фирмата, но т.нар [tech debt](https://en.wikipedia.org/wiki/Technical_debt) (както, надяваме се, сами виждате) се е натрупал вече до пределно ниво, и трябва да се направи нещо по въпроса. Ще влезем в ролята на ваши новоназначени tech lead-ове и ще ви позволим (даже задължим) да оправите бъркотията и спагетите в съществуващия код (yes, pun intended). 111 | 112 | Полученото можем да оценим по сравнително обективен начин, чрез `pylint`. Изпълнете следната команда (след `pip install pylint`, ако нямате инсталиран [pylint](https://pypi.org/project/pylint/)): 113 | 114 | ```bash 115 | pylint spaghetti-notes # ако сте над директорията на проекта 116 | # или 117 | pylint main.py actions/ # ако сте във директорията на проекта 118 | ``` 119 | 120 | Това ще ви изведе серия от грешки, предупрежнения, съвети и т.н. по стила на кода. Накрая ще има един ред с обща оценка от рода на `Your code has been rated at 5.89/10.` 121 | 122 | Разбира се, трябва да го сведете до **10/10**. 123 | 124 | ### Цел Г: Писане на тестове 125 | 126 | За да не се допускат в бъдеще глупави бъгове като този от Цел А е хубаво да се напишат тестове. В тази задача това е и почти невъзможно без изпълнението и на Цел В - двете е най-естествено да вървят ръка за ръка. 127 | 128 | Напишете изчерпателни юнит тестове за всеки един от основните компоненти/функционалности на програмата, плюс каквито нови отделите/създадете. Използвайте който testing framework предпочитате без ограничения. 129 | 130 | Можете, разбира се, да създавате папки и местите файлове както прецените за удачно. 131 | 132 | За упражнението ще изискаме и да проверявате тестовете колко реда от кода ви покриват чрез [`coverage`](https://coverage.readthedocs.io/en/7.6.10/) (инсталирате с `pip install coverage`). В [документацията](https://coverage.readthedocs.io/en/7.6.10/) е хубаво описано как се използва - общо взето в командата, с която си пускате тестовете, трябва да замените `python -m ...` с `coverage run -m ...`, а след това да пуснете `coverage report` за да видите статистика за code coverage (`coverage report -m` ви дава и кои редове са изтървани): 133 | 134 | * Ако използвате `unittest`: 135 | 136 | ```bash 137 | coverage run -m unittest ... 138 | coverage report -m 139 | ``` 140 | 141 | * Ако използвате `pytest`: 142 | 143 | ```bash 144 | coverage run -m pytest ... 145 | coverage report -m 146 | ``` 147 | 148 | *Ако не сте сигурни какво да напишете на мястото на `...` си припомнете [лекцията за тестове](../../14%20-%20Testing/15%20-%20Testing.ipynb).* 149 | 150 | Втората команда извежда таблица с покритието, като **изискваме на TOTAL да имате поне 80%.** 151 | 152 | Ако сорс кодът ви се намира в директория на същото ниво на директорията с тестовете (препоръчително), можете да укажете `--source=.` като аргумент на `coverage run`. Справка: [тук](https://coverage.readthedocs.io/en/7.6.10/cmd.html#cmd-run). 153 | 154 | ## Оценяване 155 | 156 | Общо макс. 3т.: 157 | 158 | * Цел А: 0.5т. (след code review) 159 | * Цел Б: 0.5т. (след code review) 160 | * Цел В: 1т. (след code review + pylint 10/10) 161 | * Цел Г: 1т. (след code review + coverage >= 80%) 162 | -------------------------------------------------------------------------------- /01 - Intro to Python/install-n-setup.md: -------------------------------------------------------------------------------- 1 | # Въведение в Python и настройка на средата 2 | 3 | ## Какво е Python? 4 | 5 | ![Python and a python](assets/pythons.png) 6 | 7 | Python е [high-level](https://www.geeksforgeeks.org/difference-between-high-level-and-low-level-languages/) език за програмиране, който е [интерпретируем](https://www.geeksforgeeks.org/difference-between-compiler-and-interpreter/), [динамично-типизиран](https://www.educative.io/answers/what-is-dynamic-typing#) и мултипарадигмен. Синтаксисът му е семпъл и лесен за научаване. Има интеграция със С код и библиотеки. Комибнацията от всичко изброено прави Python един мощен език за програмиране. 8 | 9 | ### За какво се ползва? 10 | 11 | * **Скриптове** (автоматизация, DevOps, системна администрация, data flows & pipelines, инфраструктурни процеси, конзолни инструменти, тестване, т.н.) 12 | * **Machine Learning / Data Science / AI** (посредством библиотеки като TensorFlow, PyTorch, Scikit-learn, Pandas, NumPy, SciPy, Matplotlib и т.н.) 13 | * **Уеб сървъри** (бекенд, посредством библиотеки като Django, Flask, FastAPI, т.н.) 14 | * **Desktop приложения** (рядко) 15 | 16 | ### Версии 17 | 18 | * **0.9** (1991) - първа версия 19 | * **1.0** (1994) 20 | * **2.0** (2000) 21 | * **3.0** (2008) 22 | * **2.7** (2010) - последната minor версия на Python 2 23 | * **3.9** (окт. 2020) 24 | * **3.10** (окт. 2021) 25 | * **3.11** (окт. 2022) 26 | * **3.12** (окт. 2023) 27 | * 3.13 планирана за окт. 2024 28 | * 4.0 [може и да няма](https://builtin.com/software-engineering-perspectives/python-4) 29 | 30 | ### Какви са минусите на Python? 31 | 32 | * **Бавен** - в пъти даже, в сравнение с езици като C/C++ и Java (макар и в 3.11 да са направени подобрения на скоростта, няма как да се сравнява с по-low-level езици) 33 | * **Multi-threading-ът е неефективен** - това ще го разберем защо е така [по-късно в курса](../09%20-%20Multithreading/). 34 | * **Не е особено подходящ за стабилни и скалируеми backend-и** - заради типизация, скорост, конкурентност и други причини 35 | * **(Почти) не може да се пишат мобилни приложения на него** 36 | 37 | ### Какви са плюсовете на Python? 38 | 39 | * **Лесен синтаксис** 40 | * **Мощен** - с малко код могат да се направят много неща 41 | * **Мултипарадигмен** - поддържа функционален, обектно-ориентирен и процедурен стил на програмиране 42 | * **Мултиплатформен** - работи на Windows, Linux, Mac и се **съдържа по подразбиране в Linux** дистрибуциите 43 | * **Най-популярният** език (конкурира се с JavaScript) [от няколко години насам](https://youtu.be/qQXXI5QFUfw?si=rN7-FmBV5FWPi_zL&t=392) (главният заподозрян за това е AI и Data Science бума) - това означава, че ако ви трябва библиотека за нещо, то най-вероятно вече я има (както и наличието огромното community от хора, които да отговарят на въпроси и т.н) 44 | * **C interoperability** - може да работи със С код и така да използва всичките предимства на по-low-level езикът за програмиране (най-вече бързодействие). И това ще го разберем как става към [края на курса](../16%20-%20Using%20C%20code%20in%20Python/) 45 | 46 | ## Инсталиране на Python 47 | 48 | ### Windows 49 | 50 | 0. ~~Минете на Linux~~ 51 | 1. Изтеглете последната версия на Python от [официалния сайт](https://www.python.org/downloads/windows/) 52 | 2. Инсталирайте Python, като отбележите опцията **Add Python 3.x to PATH** (това е с цел да може да изпълняваме `python` от командния ред) 53 | 3. Оставете всичко друго по подразбиране и цъкнете Install Now. 54 | 55 | Ако всичко е приключило успешно, би трябвало да имате вече програма Python IDLE (или Python Shell), която като отворите ще ви покаже интерактивна конзола, в която можете да изпробвате разични Python изрази: 56 | 57 | ![Python Shell GUI screenshot](assets/idle.png) 58 | 59 | Същото нещо би трябвало да можете да достъпите и през терминала (понеже сме отметнали опцията *Add Python 3.x to PATH*), като изпълните командата `python`: 60 | 61 | ![Python Shell CLI screenshot](assets/pythoncmd.png) 62 | 63 | (излиза се с `exit()`) 64 | 65 | ### Linux 66 | 67 | Ако нямате вече `python`/`python3`, то или дистрибуцията ви е крайно прецакана, или имате някакво твърде obscure Linux distro. За да видите дали вече имате Python 3 инсталиран (и конкретната му версия), изпълнете командата: 68 | 69 | ```bash 70 | python3 --version 71 | ``` 72 | 73 | ⚠️ **Важно:** В курсът изучаваме само Python 3. Командата `python` (без тройката) също съществува, но тя подкарва Python 2, който е deprecated и не трябва да се използва повече. 74 | 75 | И все пак, ако случайно нямате Python, то може да го инсталирате чрез специфичния за дистрибуцията ви package manager. Някои по-популярни примери: 76 | 77 | * **Debian/Ubuntu** - `sudo apt install python3` 78 | * **Fedora** - `sudo dnf install python3` 79 | * **Arch/Manjaro** - `sudo pacman -S python` 80 | 81 | ### macOS 82 | 83 | Също както при линукс, проверете дали имате вече `python3` командата, чрез: 84 | 85 | ```bash 86 | python3 --version 87 | ``` 88 | 89 | В случай, че видите точна версия - поздравления, вече сте сложили Python 3. Ако пък командата гръмне, то най-вероятно нямате Xcode Command-line Tools. Те идват вградени с [Xcode](https://apps.apple.com/bg/app/xcode/id497799835?mt=12), но могат и да бъдат инсталирани отделно самостоятелно чрез: 90 | ```bash 91 | xcode-select --install 92 | ``` 93 | С тях получавате `python`, `git` и доста други developer-ски благинки. 94 | 95 | ## Инсталиране и настройка на редактор / IDE 96 | 97 | Тук са описани стъпки за инсталиране на две програми за разработка на Python код - Visual Studio Code и PyCharm. 98 | 99 | Как да избера кое да ползвам? 100 | 101 | | VSCode | PyCharm | 102 | | ------ | ------- | 103 | | ✅ Безплатна | ✅ Безплатна за студенти от ФМИ* | 104 | | ⚙️ По-конфигурируема | 🚀 По-лесна за настройка | 105 | | 🐙 За всякакви езици | 🐍 Специализирана за Python | 106 | | 🪶 По-лека** | 🐳 По-тежка | 107 | 108 | 109 | (*) Community edition е безплатна версия за всички; Professional edition е платена, но при регистрация с университетския имейл получавате безплатен лиценз 110 | 111 | (**) освен ако не ѝ се инсталират допълнително твърде много тежки плъгини 112 | 113 | ### Visual Studio Code 114 | 115 | VSCode е удобен **текстов редактор**, специално създаден за редактиране на сорс код. Той е лек, бърз и лесно конфигурируем за всякакви езици и нужди, чрез мнжеството разширения (още extensions/plugins), които могат да му се инсталират. 116 | 117 | 1. Свалете VSCode от [официалния сайт](https://code.visualstudio.com/download) 118 | 2. Инсталирайте го 119 | 3. Отворете VSCode 120 | 4. Oтворете Extensions Marketplace панела, по един от следните три начина: 121 | * от лентата с инструменти вляво (иконката с формата на тетрис квадратчета) 122 | * от менюто `View > Extensions` 123 | * с Ctrl+Shift+X (или ⌘⇧X на macOS) 124 | 5. Потърсете и изтеглете следните 3 плъгина: 125 | * [Code Runner](https://marketplace.visualstudio.com/items?itemName=formulahendry.code-runner) (за пускане и дебъгване на код) 126 | * [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) (за Python syntax highlighting и др.) 127 | * [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) (за Python intellisense, autocompletion и др.) 128 | * (по желание) [Jupyter](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) (за работа с Jupyter notebooks, в какъвто формат са и лекциите в курса) 129 | 130 | ![VSCode screenshot](assets/vscode.png) 131 | 132 | #### Първият ви Python файл 133 | 134 | 1. Отворете VSCode 135 | 2. Отворете нов файл (File > New File) (или с Ctrl+N / ⌘N) 136 | 3. Напишете `print("Hello, world!")` 137 | 4. Запазете файла (File > Save) (или с Ctrl+S / ⌘S) 138 | 5. Пуснете го с Run > Run without Debugging 139 | 140 | ### PyCharm 141 | 142 | PyCharm е **интегрирана среда за разработка** (IDE), специално създадена за Python. Тя е много по-тежка от VSCode, но идва с много повече вградени функционалности, които са полезни за разработка на по-големи проекти. 143 | 144 | Tя е продукт на JetBrains, която е известна със своите IDE-та за всякакви езици и проекти (напр. IntelliJ за Java, CLion за C/C++, WebStorm за JavaScript и т.н.), които са визуално и функционално подобни помежду си (което е плюс за хора, свикнали да ползват друго тяхно IDE). 145 | 146 | Можете да свалите PyCharm от [официалния сайт на JetBrains](https://www.jetbrains.com/pycharm/download/). Има две версии - Community и Professional. Community е безплатна, но е по-ограничена във функционалностите си. Professional е платена, но студентите от ФМИ могат да си вземат безплатен лиценз, като се регистрират с университетски имейл. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Курс "Програмиране с Python" 2024 2 | 3 | ![Logo](misc/logo.png) 4 | 5 | Github repository към курса "Програмиране с Python" във ФМИ 6 | 7 | email: pythoncoursefmi@gmail.com 8 | 9 | [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/HwXcZuvZbq) 10 | 11 | ## Провеждане 12 | 13 | - понеделник: 18:00-20:00 @ зала 325 14 | - четвъртък: 19:00-21:00 @ зала 101 15 | 16 | ### Лекции 17 | 18 | Всички лекции са събрани под формата на Jupyter notebook интерактивни записки. 19 | 20 | JupyterBook "книжка" с всички теми има тук: https://fmipython.github.io/PythonCourse2024 21 | 22 | | Тема номер | Тема | Дата | Лектор | 23 | | ---------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 24 | | 0 | [Въведение към курса](./00%20-%20Course%20intro/) | 03.10.2024 | [Любо](https://github.com/lyubolp) & [Алекс И.](https://github.com/yalishanda42) & [Кари](https://github.com/karinaghristova) & [Алекс К.](https://github.com/Bladwark) | 25 | | 1 | [Въведение в Python: какво е Python, настройка на средата, как да пуснем лекциите](./01%20-%20Intro%20to%20Python/) | 03.10.2024 | [Любо](https://github.com/lyubolp) & [Алекс И.](https://github.com/yalishanda42) & [Кари](https://github.com/karinaghristova) & [Алекс К.](https://github.com/Bladwark) | 26 | | 2 | [Променливи, разклонения, цикли](./02%20-%20Variables,%20types,%20control%20flow/) | 07.10.2024, 10.10.2024 | [Алекс](https://github.com/yalishanda42) & [Кари](https://github.com/karinaghristova) | 27 | | 3 | [Обектно-ориентирано програмиране в Python](./03%20-%20OOP/) | 14.10.2024, 17.10.2024 | [Алекс](https://github.com/yalishanda42) | 28 | | 4 | [Функционално програмиране в Python](./04%20-%20Functional%20Programming/) | 24.10.2024, 28.10.2024 | [Любо](https://github.com/lyubolp) | 29 | | 5 | [Представяне на структури от данни и алгоритми над тях](./05%20-%20Data%20Structures%20and%20Oddities/) | 04.11.2024, 07.11.2024 | [Любо](https://github.com/lyubolp) | 30 | | 6 | [Типова система на езика](./06%20-%20Typing%20Hints/) | 14.11.2024 | [Алекс](https://github.com/yalishanda42) | 31 | | 7 | [Грешки и изключения](./07%20-%20Exceptions%20Handling/) | 18.11.2024 | [Алекс](https://github.com/yalishanda42) | 32 | | 8 | [Работа с файлове](./08%20-%20Files/) | 21.11.2024 | [Любо](https://github.com/lyubolp) | 33 | | 9 | [Многонишково програмиране](./09%20-%20Multithreading/) | 02.12.2024 | [Любо](https://github.com/lyubolp) | 34 | | 10 | [Работа със заявки](./10%20-%20requests/) | 05.12.2024 | [Алекс](https://github.com/yalishanda42) | 35 | | 11 | [Работа с Git](./11%20-%20Git/) | 05.12.2024 | [Алекс](https://github.com/yalishanda42) | 36 | | 12 | [Модули и пакети](./12%20-%20Modules/) | 09.12.2024 | [Алекс](https://github.com/yalishanda42) | 37 | | 13 | [Принципи на качествения код на Python](./13%20-%20Clean%20code/) | 12.12.2024 | [Любо](https://github.com/lyubolp) | 38 | | 14 | [Тестване в Python](./14%20-%20Testing/) | 19.12.2025 | [Алекс](https://github.com/yalishanda42) | 39 | | 15 | [Уеб програмиране. Flask](./15%20-%20Web%20programming/) | 06.01.2025 | [Любо](https://github.com/lyubolp) | 40 | | 16 | [Използване на C код в Python](./16%20-%20Using%20C%20code%20in%20Python/) | 13.01.2025 | [Любо](https://github.com/lyubolp) | 41 | | 17 | [Външни библиотеки (numpy, pandas, matplotlib)](./17%20-%20numpy,%20pandas,%20matplotlib/) | 16.01.2025 | [Любо](https://github.com/lyubolp) | 42 | 43 | ### Упражнения 44 | 45 | Вместо лекции, на някои дати ще се провеждат специални упражнения, на които ще се решават задачи за текущ контрол. График: 46 | 47 | | № | Дата | Теми | Точки | Линк | 48 | | --- | ---------- | ------------- | ----- | ------------------------------------------------------------------------------------------ | 49 | | 1 | 21.10.2024 | 2, 3 | 5т | [Задачи](https://github.com/fmipython/PythonCourse2024/blob/master/labs/lab01/lab01.ipynb) | 50 | | 2 | 31.10.2024 | 4 | 5т | [Задачи](https://github.com/fmipython/PythonCourse2024/blob/master/labs/lab02/lab02.ipynb) | 51 | | 3 | 11.11.2024 | 5 | 3т | [Задачи](https://github.com/fmipython/PythonCourse2024/blob/master/labs/lab03/lab03.md) | 52 | | 4 | 28.11.2024 | 6, 7, 8 | 3т | [Задачи](https://github.com/fmipython/PythonCourse2024/blob/master/labs/lab04/lab04.ipynb) | 53 | | 5 | 16.12.2024 | 9, 10, 11, 12 | 5т | [Задачи](https://github.com/fmipython/PythonCourse2024/blob/master/labs/lab05/lab05.md) | 54 | | 6 | 09.01.2024 | 13, 14 | 3т | [Задачи](https://youtu.be/ARJ8cAGm6JE?feature=shared&t=63) | 55 | 56 | ### Тестове 57 | 58 | На тези дати ще провеждаме тестовете в курса. Тестовете ще са няколко въпроса, в рамките на няколко минути. Провеждането им ще е в Moodle. 59 | | № | Дата | Теми | 60 | |---|------------|------| 61 | |1 | 14.10.2024 | 1, 2 | 62 | |2 | 24.10.2024 | 3 | 63 | |3 | 04.11.2024 | 4 | 64 | |4 | 14.11.2024 | 5 | 65 | |5 | 18.11.2024 | 6 | 66 | |6 | 21.11.2024 | 7 | 67 | |7 | 02.12.2024 | 8 | 68 | |8 | 05.12.2024 | 9 | 69 | |9 | 19.12.2024 | 12 | 70 | |10 | 13.01.2025 | 14 | 71 | 72 | ## Оценяване 73 | 74 | Крайната оценка (мин 2, макс 6) се изчислява по следната формула: 75 | 76 | $$ Оценка = \frac{Упражнения + Тестове + Бонус + Проект}{10} $$ 77 | 78 | Нужно е да отбележим, че **нито един от четирите компонента не е задължителен** за успешното взимане на курса. 79 | 80 | Максимални точки: 81 | 82 | - Упражнения: 25т. 83 | - Тестове: 10т. 84 | - Бонус: 5т. 85 | - Проект: 40т. 86 | 87 | # Как да пиша и подкарам Python кода си? 88 | 89 | Вж. [тук](./01%20-%20Intro%20to%20Python/install-n-setup.md) 90 | 91 | # Как да си пуснем лекциите/материалите? 92 | 93 | Вж. [тук](./01%20-%20Intro%20to%20Python/notebooks.md) 94 | 95 | # Оценяване на проектите? 96 | 97 | Вж. [тук](./projects.md) 98 | 99 | # Примерни проекти? 100 | 101 | Вж. [тук](./example_projects.md) 102 | 103 | # Принос 104 | 105 | Ако откриете бъг, правописна грешка или генерално нещо грешно, може да отворите pull request чрез съответен branch съдържащ номера на лекцията. При промяна на някоя от тетрадките, задължително изпълнете тетрадката отначало преди качването в Git (за да са подредени номерцата на output-ите). 106 | -------------------------------------------------------------------------------- /11 - Git/README.md: -------------------------------------------------------------------------------- 1 | # Работа с Git 2 | 3 | Тази тема не е пряко свързана с Python, но е необходимо всеки от вас да е усвоил понятията от нея, понеже: 4 | 1. Всички домашни се предават чрез GitHub Classroom, което изисква да се работи с Git. 5 | 2. Version control системи на работа винаги се използват и много рядко са нещо различно от Git (освен ако нямате късмета на Любо). 6 | 7 | ## Какво е Git? 8 | 9 | Децентрализирана система за контрол на версиите на сорс кода. 10 | 11 | Накратко: това означава, че може да се възстановяват предишни версии на кода, като се пази и историята на промените на всеки файл от проекта. Git (и други системи за контрол на версиите) са причината хората да не си разменят zip файлчета с код насам-натам и да не получават инфаркт ако се направи фатална грешка, след която кодът на проекта трябва да се върне до предишно по-стабилно състояние. Git ни предоставя начин за колабориране с други колеги, чрез внасяне и сливане на промени по кода по прозрачен начин и обратим начин. 12 | 13 | ## Как да изтегля Git? 14 | 15 | `git` е командата, която се използва за да управляваме repository-та чрез Git. Тя има много подкоманди, които се използват за различни цели. Сред най-използваните са `git clone`, `git add`, `git commit`, `git push`, `git pull`, `git merge` и други. Ще ги обясним подробно малко по-нататък. 16 | 17 | * **Windows**: препоръчва се инсталирането на [Git Bash](https://git-scm.com/download/win), което инсталира `git` и предоставя терминал, който позволява използването на `bash` команди и синтаксис и е доста по-удобен от `cmd.exe` примерно. 18 | * **Linux**: би трябвало да го има вече за повечето дистрибуции. Ако все пак го няма: копи-пейст на съответната команда [оттука](https://git-scm.com/download/linux): 19 | * **macOS**: ако имате XCode command-line tools инсталирани (които идват с XCode), значи би трябвало да имате `git`. Ако все пак нямате, ползвайте Homebrew да си го сложите: `brew install git`. 20 | 21 | ## Как да управлявам папки с Git? 22 | 23 | За да бъде една папка, както и подпапките ѝ, поддържана от Git, трябва да съществува скритата папка `.git` в нея с необходимите за работата на Git файлове. Там се пазят т.нар. "refs" (references) към съдържанието на всички файлове и историята на техните промени. 24 | 25 | Такава папка се нарича Git repository. За да създадем такава, трябва да изпълним командата [`git init`](https://www.atlassian.com/git/tutorials/setting-up-a-repository/git-init) в root-a на папката, която искаме да поддържаме с Git (т.е. тази, която да стане нашето repository, заедно с подпапките си). 26 | 27 | ## Как се пази и следи историята? 28 | 29 | Промените в Git се разделят на т.нар. ***commit***-и. 30 | 31 | Всеки commit съдържа: 32 | 33 | * **hash**: уникален идентификатор (хешкод) на commit-a, който се генерира автоматично. 34 | * **message**: кратко описание на промените 35 | * **parents**: кой/кои са предшествениците на този commit. Всеки commit има обикновено точно един предшественик (освен първия, който няма такива). Един commit може да има няколко предшественика, ако е в следствие на ***merge*** операция например. 36 | * **author**: кой е направил промените 37 | * **date**: кога са направени промените 38 | 39 | Историята на commit-ите на практика представлява нещо като дърво, в който всеки node е commit, имаш пойнтър към предишния такъв (понякога към повече от 1 предишни, в случай на merge). Тя е достъпна чрез [`git log`](https://www.atlassian.com/git/tutorials/git-log). 40 | 41 | Всеки commit се асоциира и със съответните промени, които той донася в кодбазата (т.е. във файловете в repository-то). Това се нарича ***diff***. Различните видове промени биват добавяне и премахване на ред, добавяне, изтриване и преименуване на файл. Чрез [`git diff`](https://www.atlassian.com/git/tutorials/saving-changes/git-diff) може да се направи сравнение между две версии на файлове. 42 | 43 | Ако искаме да отидем до някой специфичен къмит, използваме `git checkout `. Това ще промени съдържанието на файловете в директорията, така че да отговарят на състоянието на кода в този commit. 44 | 45 | Обикновено, за да можем паралелно да работим с други хора, или пък да пазим различни състояние едновременно, искаме да използваме различни бранчове (branches). Един branch на практика представлява пойнтър към даден commit. Управлението им става чрез [`git branch`](https://www.atlassian.com/git/tutorials/using-branches), а преминаването от един в друг - чрез [`git checkout `](https://www.atlassian.com/git/tutorials/using-branches/git-checkout). Когато сме в даден бранч и създадем нов къмит, бранчът ще бъде променен да сочи към новия къмит (чийто предшественик е предходния) и по този начин създаваме и развиваме на практика нов свързан клон на дървото на промените. 46 | 47 | ## Как да създам нови commit-и? 48 | 49 | Да речем, че имаме папка `my-project`, която е празна. Изпълняваме `git init`, за да я добавим към Git. Изпълняваме `git status`, за да проверим на кой бранч се намираме и какво е текущото състояние на промените и индекса: 50 | 51 | ``` 52 | On branch master 53 | 54 | No commits yet 55 | 56 | nothing to commit (create/copy files and use "git add" to track) 57 | ``` 58 | 59 | Това означава, че сме в бранч на име `master` - това е името по подразбиране за главен бранч. След бунтовете от 2020 във връзка с Джордж Флойд обаче все повече се налага и името `main`, така че може и това да срещнете (an interesting read on the histeria that was then: https://github.com/rubocop/rubocop/issues/8091 (hail Bacov)). 60 | 61 | Да речем също, че искаме като за първи къмит да добавим файл `main.py` със следното съдържание: 62 | 63 | ```python 64 | print("Hello, world!") 65 | ``` 66 | 67 | Нека първия ни commit-a се казва "Initial commit". Създаването му би станало с `git commit -m "Initial commit"`. Ако се опитаме в момента обаче да го изпълним, ще получим грешка със съобщение, че нямаме никакви промени, които да къмитнем: 68 | 69 | ``` 70 | nothing added to commit but untracked files present (use "git add" to track) 71 | ``` 72 | 73 | Как така, нали добавихме файл? 74 | 75 | Това е защото първо трябва да го добавим към индекса. 76 | 77 | Т.нар. index/staging е междинно "място", съдържащо промените ни, които искаме да включим в следващия къмит (т.е. които `git commit` ще вземе). 78 | 79 | При изпълнение на `git status` в момента виждаме: 80 | 81 | ``` 82 | Untracked files: 83 | (use "git add ..." to include in what will be committed) 84 | main.py 85 | ``` 86 | 87 | "Untracked files" означава нови файлове, които досега не са били част от репозиторито. 88 | 89 | За да добавим промени/файлове към индекса, трябва да изпълним `git add `. В нашия случай - `git add main.py`. (Или `git add .` - това добавя абс. всичко към индекса). 90 | 91 | След изпълнението му, `git status` вече показва: 92 | 93 | ``` 94 | Changes to be committed: 95 | (use "git rm --cached ..." to unstage) 96 | new file: main.py 97 | ``` 98 | 99 | Премахване на файлове/промени от индекса най-общо казано става с `git restore --staged ` или с `git resed HEAD `. Това не променя файла, а просто го премахва от индекса - т.е. `git commit` просто няма да вземе тези промени, но те си остават в текущата директория. 100 | 101 | Сега вече можем да си направим къмита с `git commit -m "Initial commit"`. 102 | 103 | След като сме направили къмита, `git status` вече показва: 104 | 105 | ``` 106 | On branch master 107 | nothing to commit, working tree clean 108 | ``` 109 | 110 | С `git log` можем да видим новия къмит в дървото на историята: 111 | 112 | ``` 113 | commit 0a2ba7a9d3af36dd03a656955e2b246b9b1c8dc7 (HEAD -> master) 114 | Author: Alexander Ignatov 115 | Date: Mon Oct 3 22:02:28 2022 +0300 116 | 117 | Initial commit 118 | ``` 119 | 120 | След това за да добавим още къмити, ако трябва да обобщим, трябва да направим следните неща: 121 | 1. Добавяме промени към индекса с `git add ...` 122 | 2. Къмитваме промените с `git commit -m "Commit message"` (или `git commit` - това ще отвори текстов редактор, в който ще трябва да напишете съобщението си. При невъзможност за излизане от `vim`, моля позвънете на 112). 123 | 124 | ![diagram](./local-changes.png) 125 | 126 | ## Как да кача/сваля промените си към/от друго репозитори? 127 | 128 | Git е децентрализиран. Към всяко едно репозитори могат да се добавят един или повече т.нар. remotes. Това са други репозиторита, с които нашето може да синхронизира къмити и бранчове. Те се добавят с `git remote add `. Общоприето е да се използва `origin` за името на remote-а, ако е един и/или има централна роля. 129 | 130 | За пример, ако имаме репозитори в GitHub създадено и искаме да го свържем с нашето, трябва да го добавим чрез HTTPS линка му с `git remote add origin https://github.com/user/reponame`. (SSH е предпочитан с цел по-добра сигурност). Това свързване става автоматично, ако нямаме локална копие на репото и използваме `git clone` за да го създадем, например в този случай `git clone https://github.com/user/reponame`, 131 | 132 | За да свържем наш локален бранч към такъв, намиращ се в `origin`, трябва да изпълним `git branch --set-upstream-to=origin/ `. Това ще създаде локален бранч с името на remote-а и името на бранча, към който сме свързали нашия. Това е нещо, което може да се направи и автоматично, като се отиде на съответния бранч с `git checkout `, ако `` е име на бранч, който още нямаме локално, но вече съществува на remote-a. 133 | 134 | За да качим промените си, трябва да изпълним `git push`. Това ще качи всички промени, които имаме локално на текущия бранч, към remote-а, към който сме свързани, и на бранча, към който текущия е свързан. В случай, че не е свързан с никой, то тогава ще трябва да изпълним `git push -u origin `, където `` е името на бранча, намиращ се в origin, към който искаме да качим промените си (обикновено същото име като текущия локален). 135 | 136 | Хубаво е периодично да се прави `git fetch`. Това обновява ref-овете с тези, намиращи се на remote-а. Това не променя локалните файлове, а само обновява информацията за къмити и бранчове, достъпни на remote-а. 137 | 138 | За да обновим наш бранч до състоянието на remote-а, трябва да изпълним `git pull` (за по-сложни репозиторита се препоръчва и с параметър `--rebase`). Това ще извлече промените от remote-а и ще ги слеят с текущия бранч. Това е еквивалентно на последователното изпълнение на `git fetch` и `git merge FETCH_HEAD`. 139 | 140 | ## Как да сливаме промени? 141 | 142 | Има два основни метода - merge и rebase. Тук ще говорим само за merge. 143 | 144 | Когато сливаме два бранча, то това означава, че искаме да включим промените от единия бранч в другия. Ако искаме например да включим промените от бранч `A` в бранч `B`, то трябва: 145 | 146 | 0. Да се уверим, че локалното съдържание в бранч А е последното, в случай, че има шанс то да е променено на remote-a (`git checkout A && git pull`). 147 | 1. Да отидем на бранч B (`git checkout B`) 148 | 2. Да изпълним `git merge A` 149 | 3. Да оправим конфликтите, ако има такива 150 | 1. В конфликтите части от файловете ще се появят merge маркери, които изглеждат по подобен начин: 151 | ``` 152 | <<<<<<< HEAD 153 | Тук е конфликтното съдържание на файла в бранча, в който сме 154 | ======= 155 | Тук е конфликтното съдържание на файла в бранча, който сливаме 156 | >>>>>>> A 157 | ``` 158 | 2. Трябва да оправим конфликтите, като изтрием всичко, което не искаме да остане и да оставим само това, което искаме да остане (Captain Obvious here) 159 | 3. След save-ване на файловете, трябва да ги добавим в индекса (`git add ...`) 160 | 4. Изпълняваме `git commit` и обикновено не променяме съобщението по подразбиране (ако се отвори `vim` и отново не можете да излезете, подайте жалба [тук](https://armenskipop.com)) 161 | 162 | Тази процедура ще създаде нов merge commit в B, който има 2 parent-a: последния къмит в А и последния такъв в B. Бранч А е успешно слят с B и вече може спокойно да бъде изтрит. 163 | 164 | ## Pro tips for people living in the terminal 165 | 166 | 1. За улеснение препоръчваме използването на теми и плъгини за терминала, които показват като част от prompt-a информация като това текущата директория дали е под git, в кой бранч се намира в момента и т.н. В Git Bash това е вградено, а за `zsh` (или `bash`) може да изтеглят framework-ове и плъгини, като например [oh-my-zsh](https://ohmyz.sh). 167 | 168 | 2. [`diff-so-fancy`](https://github.com/so-fancy/diff-so-fancy) е tool, който прави четенето на diff-ове в терминала една идея по-човешко (за който е hardcore и държи да си ползва терминала вместо някой GUI като [SourceTree](https://www.sourcetreeapp.com) например или дори вградената Git функционалност на VSCode). 169 | 170 | 3. Писането на команди като `git status`, `git checkout` и т.н. бързо се превръща в нещо, което ни се иска да не ни отнема толкова време да пишем. За да се спести време, може да се използват alias-и, които са съкращения на командите. Например, ако искаме да съкратим `git status` до `gs`, можем да напишем във файла с настройките на терминала (`.bashrc` или `.zshrc`) следното: 171 | ```bash 172 | alias gs='git status' 173 | ``` 174 | 175 | 4. Идеи за alias-и, теми, плъгини и т.н. за `zsh` и `oh-my-zsh` може да почерпите от нечии dotfiles, като например [моите](https://github.com/yalishanda42/dotfiles). 176 | -------------------------------------------------------------------------------- /labs/lab04/lab04.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "**Важно !**: Във всяка задача е задължително използването на type hints в дефинициите на функциите." 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## Задача 1 (1т.)" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "Напишете функция `validate_list`, която приема път до списък за пазаруване. Списъкът е съставен от име на предмета, бройката която трябва да се закупи и единичната му цена. Функцията трябва да валидира списъка по зададени условия, и ако списъка е валиден, да върне общата сума, която е необходимо да похарчим. Ако списъка не отговаря на някое от условията, се хвърля специално дефинирана грешка.\n", 22 | "\n", 23 | "Възможните грешки са:\n", 24 | "- `InvalidLineError` - приема реда, който не отговаря на условията\n", 25 | "- `InvalidItemError` - приема името на предмета, който не отговаря на условията\n", 26 | "- `InvalidQuantityError` - приема броя и предмета, който не отговаря на условията\n", 27 | "- `InvalidPriceError` - приема цената и предмета, който не отговаря на условията\n", 28 | "- `ListFileError` - приема пътя до файла, който не отговаря на условията\n", 29 | "\n", 30 | "\n", 31 | "Правилата за валидиране на следните:\n", 32 | "- Ако файла не съществува, хвърлете `ListFileError`.\n", 33 | "- Ако файла не може да бъде прочетен, хвърлете `ListFileError`.\n", 34 | "- Всеки ред от файла започва с `-`. Ако не е така, хвърлете `InvalidLineError`.\n", 35 | "- Всеки ред има следната структура: `име на предмета:брой:единична цена`. Ако не е така, хвърлете `InvalidLineError`.\n", 36 | "- Ако името на предмета е празен низ, или е съставено само от цифри, хвърлете `InvalidItemError`.\n", 37 | "- Ако броят не е число, хвърлете `InvalidQuantityError`.\n", 38 | "- Ако броят не е цяло число, хвърлете `InvalidQuantityError`.\n", 39 | "- Ако броят е отрицателно, хвърлете `InvalidQuantityError`.\n", 40 | "- Ако единичната цена не е число, хвърлете `InvalidPriceError`.\n", 41 | "- Ако единичната цена е отрицателно число, хвърлете `InvalidPriceError`.\n", 42 | "\n", 43 | "\n", 44 | "**Примерен файл:**\n", 45 | "```\n", 46 | "- мляко:2:2.50\n", 47 | "- хляб:1:1.50\n", 48 | "- банани:1:2.50\n", 49 | "- ябълки:1:0.50\n", 50 | "- круши:1:1.75\n", 51 | "```\n", 52 | "\n", 53 | "Примерни файлове може да намерите в папката `lab04/task_1`" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 12, 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "# Write your code here" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "assert abs(validate_list(os.path.join(\"task_1\", \"list1.txt\")) - 11.25) < 0.001\n", 72 | "\n", 73 | "assert int(validate_list(os.path.join(\"task_1\", \"list2.txt\"))) == 0, \"Empty files should return 0\"\n", 74 | "\n", 75 | "try:\n", 76 | " validate_list(os.path.join(\"task_1\", \"list3.txt\"))\n", 77 | " assert False, \"Should raise InvalidLineError\"\n", 78 | "except InvalidLineError:\n", 79 | " pass\n", 80 | "\n", 81 | "try:\n", 82 | " validate_list(os.path.join(\"task_1\", \"list4.txt\"))\n", 83 | " assert False, \"Should raise InvalidLineError\"\n", 84 | "except InvalidLineError:\n", 85 | " pass\n", 86 | "\n", 87 | "try:\n", 88 | " validate_list(os.path.join(\"task_1\", \"list5.txt\"))\n", 89 | " assert False, \"Should raise InvalidLineError\"\n", 90 | "except InvalidItemError:\n", 91 | " pass\n", 92 | "\n", 93 | "try:\n", 94 | " validate_list(os.path.join(\"task_1\", \"list6.txt\"))\n", 95 | " assert False, \"Should raise InvalidLineError\"\n", 96 | "except InvalidQuantityError:\n", 97 | " pass\n", 98 | "\n", 99 | "try:\n", 100 | " validate_list(os.path.join(\"task_1\", \"list7.txt\"))\n", 101 | " assert False, \"Should raise InvalidLineError\"\n", 102 | "except InvalidQuantityError:\n", 103 | " pass\n", 104 | "\n", 105 | "try:\n", 106 | " validate_list(os.path.join(\"task_1\", \"list8.txt\"))\n", 107 | " assert False, \"Should raise InvalidLineError\"\n", 108 | "except InvalidQuantityError:\n", 109 | " pass\n", 110 | "\n", 111 | "try:\n", 112 | " validate_list(os.path.join(\"task_1\", \"list9.txt\"))\n", 113 | " assert False, \"Should raise InvalidLineError\"\n", 114 | "except InvalidPriceError:\n", 115 | " pass\n", 116 | "\n", 117 | "try:\n", 118 | " validate_list(os.path.join(\"task_1\", \"list10.txt\"))\n", 119 | " assert False, \"Should raise InvalidLineError\"\n", 120 | "except InvalidPriceError:\n", 121 | " pass\n", 122 | "\n", 123 | "try:\n", 124 | " validate_list(os.path.join(\"task_1\", \"list11.txt\"))\n", 125 | " assert False, \"Should raise InvalidLineError\"\n", 126 | "except InvalidLineError:\n", 127 | " pass\n", 128 | "\n", 129 | "\"✅ All OK! +1 point\"" 130 | ] 131 | }, 132 | { 133 | "cell_type": "markdown", 134 | "metadata": {}, 135 | "source": [ 136 | "## Задача 2 (2т.)" 137 | ] 138 | }, 139 | { 140 | "cell_type": "markdown", 141 | "metadata": {}, 142 | "source": [ 143 | "`LAIKA` е алгоритъм с *неизвестни* създадели, който има за цел да кодира и дистрибутира информация.\n", 144 | "\n", 145 | "`LAIKA` алгоритъма работа с низове, като ги разделя и кодира на отделни парчета.\n", 146 | "\n", 147 | "Кодирането е симетрично, като първият символ се поставя на първа позиция, следващият на последна позиция, този след него на 2-ра, този след него на предпоследна, и т.н.:\n", 148 | "\n", 149 | "\"abcdefg\" => \"acegfdb\"\n", 150 | "\n", 151 | "`LAIKA` се нуждае и от параметър `n`, който разделя **кодираното** съобщение на части с размер `n`.\n", 152 | "\n", 153 | "Например, прилагайки `LAIKA` с разделяне на части с дължина 5, върху низът \"Hello, this is a secret message\", ще получим следният резултат:\n", 154 | "\n", 155 | "`\"Hello, this is a secret message\" => \"Hlo hsi ertmsaegse ecsas it,le\" => 'Hlo h', 'si e', 'rtmsa', 'egse ', 'ecsas', ' it,l', 'e'` (вторият елемент има два последователни символа за празно място)\n", 156 | "\n", 157 | "Така, за да може да се разчете нашето съобщение, ще са необходими всичките му части.\n", 158 | "\n", 159 | "От вас се изисква да имплементирате алгоритъма, като създадете клас `LAIKA`, със следните методи:\n", 160 | "\n", 161 | "- Конструкторът на класът приема два аргумента - път до директория, в която ще се съхраняват резултатите файлове (повече за тях, по-долу), и цяло число `caesar_key`.\n", 162 | "- `encode` - приема низ и цяло число `n`, и връща резултата от `LAIKA` кодирането върху него\n", 163 | "- `decode` - приема списък от низове кодирани с `LAIKA`, и връща оригиналния вход\n", 164 | "\n", 165 | "- `encode_to_files` - приема низ и цяло число `n`, кодира съдържанието, и записва всяка една от отделните части в отделни файлове под директорията, подадена в конструктора. Като резултат връща името на **първия** файл от поредицата. Ако някой от файлове вече съществува, трябва да се хвърли `FileExistsError`\n", 166 | "\n", 167 | "- `decode_from_files` - приема името на **първият** файл от поредица кодирани файлове. Прочита файлове и декодира тяхното съдържание. Връща низ с оригиналното съдържание. Ако някой от очакваните файлове не съществува, трябва да се хвърли `FileNotFoundError`.\n", 168 | "\n", 169 | "Структурата на всеки от файловете е следната:\n", 170 | "- Името на файла е получено чрез прилагането на [Шифърът на Цезар](https://en.wikipedia.org/wiki/Caesar_cipher) с ключ `caesar_key` върху съдържанието.\n", 171 | "- На първият ред от файла се записва името на **следващият** файл от поредицата. (При последният файл, просто запишете празен ред)\n", 172 | "- На вторият ред от файла се записва кодираното съдържание.\n", 173 | "\n", 174 | "### Пример\n", 175 | "Създаваме `LAIKA` обект, с аргументи `temp` (за директория), и `caesar_key=4`\n", 176 | "\n", 177 | "Ако извикаме `encode_to_files('abcdefg', 4)`, ще получим два файла - `egik` и `jhf`.\n", 178 | "Тяхното съдържание съответно е:\n", 179 | "```\n", 180 | "jhf\n", 181 | "aceg\n", 182 | "```\n", 183 | "и\n", 184 | "\n", 185 | "```\n", 186 | " \n", 187 | "fdb\n", 188 | "```\n", 189 | "\n", 190 | "То е получено по следният начин. 'abcdefg' се кодира до: `['aceg', 'fdb']`\n", 191 | "Името на първият файл е получено като приложим шифъра на Цезър върху `aceg` => `egik`.\n", 192 | "Името на вторият файл е получено по същия начин - `fdb` => `jhf`.\n", 193 | "\n", 194 | "### Бележки и подсказки:\n", 195 | "- Можем да си представим, че имаме свързан списък, от файлове. Всеки файл държи кой е следващият, както и съдържанието си. \n", 196 | "- Кодиране с шифъра на Цезър: ${\\displaystyle E_{n}(x)=(x+n)\\mod {26}}$, където $x$ е ASCII кода на буквата, а $n$ е ключа.\n", 197 | "- Приемаме,че съобщението което ще кодираме ще е съставено от букви, цифри и празни места. Няма да има табулации, празни редове и т.н.\n", 198 | "- `encode_to_files` и `decode_from_files` приемат и връщат само името на файла." 199 | ] 200 | }, 201 | { 202 | "cell_type": "code", 203 | "execution_count": 2, 204 | "metadata": {}, 205 | "outputs": [], 206 | "source": [ 207 | "# Write your code here" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": null, 213 | "metadata": {}, 214 | "outputs": [], 215 | "source": [ 216 | "# Tests\n", 217 | "\n", 218 | "# Preconditions\n", 219 | "import os\n", 220 | "\n", 221 | "root_dir = \"task_2\"\n", 222 | "os.makedirs(root_dir)\n", 223 | "\n", 224 | "l = LAIKA(root_dir, 3)\n", 225 | "\n", 226 | "# encode\n", 227 | "assert l.encode(\"abcdefg\", 2) == [\"ac\", \"eg\", \"fd\", \"b\"]\n", 228 | "assert l.encode(\"abcdefg\", 3) == [\"ace\", \"gfd\", \"b\"]\n", 229 | "assert l.encode(\"abcdefg\", 5) == [\"acegf\", \"db\"]\n", 230 | "assert l.encode(\"abcdefghijkl\", 1) == [\"a\", \"c\", \"e\", \"g\", \"i\", \"k\", \"l\", \"j\", \"h\", \"f\", \"d\", \"b\"]\n", 231 | "assert l.encode(\"abcdefghijkl\", 2) == [\"ac\", \"eg\", \"ik\", \"lj\", \"hf\", \"db\"]\n", 232 | "assert l.encode(\"abcdefghijkl\", 3) == [\"ace\", \"gik\", \"ljh\", \"fdb\"]\n", 233 | "assert l.encode(\"abcdefghijkl\", 4) == [\"aceg\", \"iklj\", \"hfdb\"]\n", 234 | "assert l.encode(\"abcdefghijkl\", 4) == [\"aceg\", \"iklj\", \"hfdb\"]\n", 235 | "assert l.encode(\"abcdefghijkl\", 12) == [\"acegikljhfdb\"]\n", 236 | "assert l.encode(\"abcdefghijkl\", 24) == [\"acegikljhfdb\"]\n", 237 | "\n", 238 | "\n", 239 | "# decode\n", 240 | "assert l.decode([\"ac\", \"eg\", \"fd\", \"b\"]) == \"abcdefg\"\n", 241 | "assert l.decode(l.encode(\"abcdefg\", 3)) == \"abcdefg\"\n", 242 | "assert l.decode(l.encode(\"abcdefg\", 5)) == \"abcdefg\"\n", 243 | "assert l.decode(l.encode(\"abcdefghijkl\", 1)) == \"abcdefghijkl\"\n", 244 | "assert l.decode(l.encode(\"abcdefghijkl\", 2)) == \"abcdefghijkl\"\n", 245 | "assert l.decode(l.encode(\"abcdefghijkl\", 3)) == \"abcdefghijkl\"\n", 246 | "assert l.decode(l.encode(\"abcdefghijkl\", 4)) == \"abcdefghijkl\"\n", 247 | "assert l.decode(l.encode(\"abcdefghijkl\", 4)) == \"abcdefghijkl\"\n", 248 | "assert l.decode(l.encode(\"abcdefghijkl\", 12)) == \"abcdefghijkl\"\n", 249 | "assert l.decode(l.encode(\"abcdefghijkl\", 24)) == \"abcdefghijkl\"\n", 250 | "\n", 251 | "\n", 252 | "# encode_to_files\n", 253 | "l1 = LAIKA(root_dir, 4)\n", 254 | "assert l1.encode_to_files(\"abcdefghijkl\", 3) == \"egi\"\n", 255 | "\n", 256 | "assert sorted(os.listdir(root_dir)) == [\"egi\", \"jhf\", \"kmo\", \"pnl\"]\n", 257 | "\n", 258 | "with open(os.path.join(root_dir, \"egi\")) as fp:\n", 259 | " next_file = fp.readline().strip()\n", 260 | " content = fp.readline().strip()\n", 261 | "\n", 262 | "assert next_file == \"kmo\"\n", 263 | "assert content == \"ace\"\n", 264 | "\n", 265 | "with open(os.path.join(root_dir, \"jhf\")) as fp:\n", 266 | " next_file = fp.readline().strip()\n", 267 | " content = fp.readline().strip()\n", 268 | "\n", 269 | "assert next_file == \"\"\n", 270 | "assert content == \"fdb\"\n", 271 | "\n", 272 | "with open(os.path.join(root_dir, \"kmo\")) as fp:\n", 273 | " next_file = fp.readline().strip()\n", 274 | " content = fp.readline().strip()\n", 275 | "\n", 276 | "assert next_file == \"pnl\"\n", 277 | "assert content == \"gik\"\n", 278 | "\n", 279 | "with open(os.path.join(root_dir, \"pnl\")) as fp:\n", 280 | " next_file = fp.readline().strip()\n", 281 | " content = fp.readline().strip()\n", 282 | "\n", 283 | "assert next_file == \"jhf\"\n", 284 | "assert content == \"ljh\"\n", 285 | "\n", 286 | "\n", 287 | "# decode_from_files\n", 288 | "assert l1.decode_from_files(\"egi\") == \"abcdefghijkl\"\n", 289 | "\n", 290 | "# Exception\n", 291 | "\n", 292 | "try:\n", 293 | " l1.encode_to_files(\"abcdefghijkl\", 3)\n", 294 | "except FileExistsError:\n", 295 | " assert True\n", 296 | "except Exception:\n", 297 | " assert False\n", 298 | "\n", 299 | "\n", 300 | "try:\n", 301 | " l1.decode_from_files(\"non-existing-file\")\n", 302 | "except FileNotFoundError:\n", 303 | " assert True\n", 304 | "except Exception:\n", 305 | " assert False\n", 306 | "\n", 307 | "print(\"✅ All OK! +2 points\")" 308 | ] 309 | } 310 | ], 311 | "metadata": { 312 | "kernelspec": { 313 | "display_name": "Python 3", 314 | "language": "python", 315 | "name": "python3" 316 | }, 317 | "language_info": { 318 | "codemirror_mode": { 319 | "name": "ipython", 320 | "version": 3 321 | }, 322 | "file_extension": ".py", 323 | "mimetype": "text/x-python", 324 | "name": "python", 325 | "nbconvert_exporter": "python", 326 | "pygments_lexer": "ipython3", 327 | "version": "3.11.0rc1" 328 | } 329 | }, 330 | "nbformat": 4, 331 | "nbformat_minor": 2 332 | } 333 | -------------------------------------------------------------------------------- /labs/lab02/lab02.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Задачи по тема 4" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "Важно ! Решенията на задачите трябва да бъдат написани изцяло във функционален стил - избягвайте цикли, мутация на данни и т.н. Позволено е да използвате list comprehension, map, filter, reduce и т.н." 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "## Задача 1 (0.75т.)" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "Даден е `namedtuple`, моделиращ точка в двумерното пространство, както и функция `calculate_area`, която връща лицето на правоъгълника дефиниран от две точки.\n", 29 | "\n", 30 | "Даден е и `namedtuple` `Rectangle`, който моделира правоъгълник, дефиниран от две точки (`start` и `end`), заедно с неговото лице (`area`).\n", 31 | "\n", 32 | "Напишете функция `get_areas`, която приема списък с начални точки, списък с крайни точки и цяло число `n`. \n", 33 | "\n", 34 | "Като резултат от функцията се очаква да се върнат:\n", 35 | "- Всички правоъгълници, които имат лице по-голямо от `n` (под правоъгълник разбираме обекти от тип `Rectangle`)\n", 36 | "- Правоъгълнците, които отговарят на условието трябва да са сортирани в намаляващ ред спрямо лицата им\n", 37 | " \n", 38 | "Пример:\n", 39 | "```python\n", 40 | "starting_points = [Point(2, 3), Point(0, 0), Point(3, 4), Point(5, 6)]\n", 41 | "ending_points = [Point(3, 4), Point(-5, -9), Point(7, 7), Point(5, 6)]\n", 42 | "\n", 43 | "get_areas(starting_points, ending_points, 9)\n", 44 | "```\n", 45 | "\n", 46 | "```\n", 47 | "[\n", 48 | " Rectangle(Point(x=3, y=4), Point(x=7, y=7), 12),\n", 49 | " Rectangle(Point(x=0, y=0), Point(x=-5, y=-9), 45), \n", 50 | "]\n", 51 | "```\n", 52 | "\n", 53 | "Лицата на правоъгълниците са 1, 45, 12 и 0. Филтрираме тези които имат лице > 9." 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 1, 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "from collections import namedtuple\n", 63 | "\n", 64 | "Point = namedtuple('Point', ['x', 'y'])\n", 65 | "Rectangle = namedtuple('Rectangle', ['start', 'end', 'area'])\n", 66 | "\n", 67 | "def calculate_area(a, b):\n", 68 | " width = abs(a.x - b.x)\n", 69 | " height = abs(a.y - b.y)\n", 70 | "\n", 71 | " return width * height" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 2, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "def get_areas(starting_points, ending_points, n):\n", 81 | " pass" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "metadata": {}, 88 | "outputs": [ 89 | { 90 | "name": "stdout", 91 | "output_type": "stream", 92 | "text": [ 93 | "✅ All OK! +0.75 point\n" 94 | ] 95 | } 96 | ], 97 | "source": [ 98 | "starting_points = [\n", 99 | " Point(2, 3), \n", 100 | " Point(0, 0), \n", 101 | " Point(3, 4), \n", 102 | " Point(5, 6),\n", 103 | " Point(3, 3),\n", 104 | "]\n", 105 | "ending_points = [\n", 106 | " Point(3, 4), \n", 107 | " Point(-5, -9), \n", 108 | " Point(7, 7), \n", 109 | " Point(5, 6),\n", 110 | " Point(0, 0),\n", 111 | "]\n", 112 | "\n", 113 | "expected_result = [\n", 114 | " Rectangle(Point(x=0, y=0), Point(x=-5, y=-9), 45), \n", 115 | " Rectangle(Point(x=3, y=4), Point(x=7, y=7), 12),\n", 116 | "]\n", 117 | "\n", 118 | "assert get_areas(starting_points, ending_points, 9) == expected_result\n", 119 | "\n", 120 | "starting_points_2 = [\n", 121 | " Point(3, 4),\n", 122 | " Point(2, 3),\n", 123 | " Point(5, 6),\n", 124 | " Point(3, 3),\n", 125 | " Point(0, 0),\n", 126 | "]\n", 127 | "ending_points_2 = [\n", 128 | " Point(7, 7),\n", 129 | " Point(3, 4),\n", 130 | " Point(5, 6),\n", 131 | " Point(0, 0),\n", 132 | " Point(-5, -9),\n", 133 | "]\n", 134 | "\n", 135 | "expected_result_2 = [\n", 136 | " Rectangle(Point(x=0, y=0), Point(x=-5, y=-9), 45),\n", 137 | " Rectangle(Point(x=3, y=4), Point(x=7, y=7), 12),\n", 138 | "]\n", 139 | "\n", 140 | "assert get_areas(starting_points_2, ending_points_2, 9) == expected_result_2\n", 141 | "\n", 142 | "print(\"✅ All OK! +0.75 point\")" 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "metadata": {}, 148 | "source": [ 149 | "## Задача 2 (1.25т.)" 150 | ] 151 | }, 152 | { 153 | "cell_type": "markdown", 154 | "metadata": {}, 155 | "source": [ 156 | "В шахa, конят се движи по един от следните начини:\n", 157 | "- две полета хоризонтално и едно вертикално\n", 158 | "- две полета вертикално и едно хоризонтално\n", 159 | "\n", 160 | "\n", 161 | "\n", 162 | "\n", 163 | "Напомняме, че редовете се номерират от 1 до 8, а колоните - от A до H. \n", 164 | "В случая, нашият кон е на позиция `d4`.\n", 165 | "Той може да се премести на следните позиции: `e7`, `f6`, `f4`, `e3`, `c3`, `b4`, `b6`, `c7`\n", 166 | "\n", 167 | "Напишете генератор, който приема начална позиция на кон, и връща всички възможни ходове." 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": 4, 173 | "metadata": {}, 174 | "outputs": [ 175 | { 176 | "name": "stdout", 177 | "output_type": "stream", 178 | "text": [ 179 | "✅ All OK! +1.25 points\n" 180 | ] 181 | } 182 | ], 183 | "source": [ 184 | "# Write your code here:\n", 185 | "def possible_moves(collumn, row):\n", 186 | " pass\n", 187 | " \n", 188 | "\n", 189 | "# 2 possible moves\n", 190 | "assert set(possible_moves('a', 1)) == {('c', 2), ('b', 3)}\n", 191 | "assert set(possible_moves('h', 1)) == {('f', 2), ('g', 3)}\n", 192 | "assert set(possible_moves('h', 8)) == {('f', 7), ('g', 6)}\n", 193 | "assert set(possible_moves('a', 8)) == {('c', 7), ('b', 6)}\n", 194 | "\n", 195 | "# 3 possible moves\n", 196 | "assert set(possible_moves('a', 2)) == {('c', 3), ('b', 4), ('c', 1)}\n", 197 | "assert set(possible_moves('a', 7)) == {('c', 6), ('b', 5), ('c', 8)}\n", 198 | "assert set(possible_moves('h', 2)) == {('g', 4), ('f', 1), ('f', 3)}\n", 199 | "assert set(possible_moves('h', 7)) == {('f', 6), ('g', 5), ('f', 8)}\n", 200 | "\n", 201 | "# 4 possible moves\n", 202 | "assert set(possible_moves('a', 3)) == {('c', 4), ('b', 5), ('b', 1), ('c', 2)}\n", 203 | "assert set(possible_moves('h', 6)) == {('g', 8), ('f', 5), ('g', 4), ('f', 7)}\n", 204 | "assert set(possible_moves('g', 2)) == {('e', 1), ('f', 4), ('h', 4), ('e', 3)}\n", 205 | "\n", 206 | "# 6 possible moves\n", 207 | "assert set(possible_moves('b', 3)) == {('c', 5), ('d', 2), ('c', 1), ('d', 4), ('a', 5), ('a', 1)}\n", 208 | "assert set(possible_moves('g', 6)) == {('f', 4), ('h', 4), ('e', 5), ('e', 7), ('h', 8), ('f', 8)}\n", 209 | "\n", 210 | "# 8 possible moves\n", 211 | "assert set(possible_moves('d', 4)) == {('b', 3), ('b', 5), ('c', 2), ('c', 6), ('e', 2), ('e', 6), ('f', 3), ('f', 5)}\n", 212 | "assert set(possible_moves('f', 6)) == {('h', 7), ('g', 8), ('e', 8), ('g', 4), ('d', 5), ('e', 4), ('h', 5), ('d', 7)}\n", 213 | "\n", 214 | "# Generator tests\n", 215 | "for move in possible_moves('a', 1):\n", 216 | " assert move in {('c', 2), ('b', 3)}\n", 217 | "\n", 218 | "generator = possible_moves('a', 1)\n", 219 | "assert next(generator) in {('c', 2), ('b', 3)}\n", 220 | "assert next(generator) in {('c', 2), ('b', 3)}\n", 221 | "\n", 222 | "try:\n", 223 | " next(generator)\n", 224 | " assert False\n", 225 | "except StopIteration:\n", 226 | " pass\n", 227 | "\n", 228 | "print(\"✅ All OK! +1.25 points\")" 229 | ] 230 | }, 231 | { 232 | "cell_type": "markdown", 233 | "metadata": {}, 234 | "source": [ 235 | "## Задача 3 (0.75 т.)" 236 | ] 237 | }, 238 | { 239 | "cell_type": "markdown", 240 | "metadata": {}, 241 | "source": [ 242 | "В езикът Rust съществува функция на име [take](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.take).\n", 243 | "\n", 244 | "Тя работи върху итератор, и връща нов итератор, с първите `n` елемента от него (или всички елементи, ако итератора е по-къс от `n`).\n", 245 | "\n", 246 | "В Python нямаме такава функция, затова нека си напишем една.\n", 247 | "\n", 248 | "Нашата Python имплементация ще приема два аргумента - `iterable` (итеруемото, от което ще взимаме елементи) и `n` (броят елементи, които ще вземем).\n", 249 | "Тя ще връща нов **генератор**, който ще съдържа само първите `n` елемента от `iterable` (или всички, ако дължината на `iterable` е под `n`).\n", 250 | "\n", 251 | "Ако `n` е отрицалтелно, нека се върне генератор с последните `n` елемента." 252 | ] 253 | }, 254 | { 255 | "cell_type": "code", 256 | "execution_count": 5, 257 | "metadata": {}, 258 | "outputs": [], 259 | "source": [ 260 | "# Write your code here\n", 261 | "def take(iterable, n):\n", 262 | " pass" 263 | ] 264 | }, 265 | { 266 | "cell_type": "code", 267 | "execution_count": null, 268 | "metadata": {}, 269 | "outputs": [ 270 | { 271 | "name": "stdout", 272 | "output_type": "stream", 273 | "text": [ 274 | "✅ All OK! +0.75 points\n" 275 | ] 276 | } 277 | ], 278 | "source": [ 279 | "sample = [1, 2, 3, 4, 5, 6]\n", 280 | "\n", 281 | "expected_1 = [1, 2, 3]\n", 282 | "actual_1 = list(take(sample, 3))\n", 283 | "\n", 284 | "expected_2 = []\n", 285 | "actual_2 = list(take(sample, 0))\n", 286 | "\n", 287 | "expected_3 = [1, 2, 3, 4, 5, 6]\n", 288 | "actual_3 = list(take(sample, 10))\n", 289 | "\n", 290 | "expected_4 = [1, 2, 3, 4]\n", 291 | "actual_4 = list(take(sample, 4))\n", 292 | "\n", 293 | "expected_5 = [5, 6]\n", 294 | "actual_5 = list(take(sample, -2))\n", 295 | "\n", 296 | "expected_6 = [3, 4, 5, 6]\n", 297 | "actual_6 = list(take(sample, -4))\n", 298 | "\n", 299 | "assert expected_1 == actual_1\n", 300 | "assert expected_2 == actual_2\n", 301 | "assert expected_3 == actual_3\n", 302 | "assert expected_4 == actual_4\n", 303 | "assert expected_5 == actual_5\n", 304 | "assert expected_6 == actual_6\n", 305 | "\n", 306 | "print(\"✅ All OK! +0.75 points\")" 307 | ] 308 | }, 309 | { 310 | "cell_type": "markdown", 311 | "metadata": {}, 312 | "source": [ 313 | "## Задача 4 (1.25т.)" 314 | ] 315 | }, 316 | { 317 | "cell_type": "markdown", 318 | "metadata": {}, 319 | "source": [ 320 | "За ефективна работа с изображения, тяхното представяне много често е във вид на матрица.\n", 321 | "Целта на тази задача е да се имплементират няколко прости операции върху изображения (матрици). \n", 322 | "\n", 323 | "За улеснение, матрица дефинираме като двумерен списък с размер `MxN` (`M` реда, и `N` колони) и елементи цели числа.\n", 324 | "\n", 325 | "Напишете следните функции, които модифицират даден матрица:\n", 326 | "\n", 327 | "- `rotate_clockwise` - завърта матрицата в посока на часовниковата стрелка\n", 328 | "- `rotate_counterclockwise` - завърта матрицата в посока обратна на часовниковата стрелка\n", 329 | "- `flip_horizontal` - връща огледална матрица, в хоризонтална посока\n", 330 | "- `flip_vertical` - връща огледална матрица, във вертикална посока\n", 331 | "\n", 332 | "\n", 333 | "Или казано по друг начин:\n", 334 | "\n", 335 | "$$ A = \\begin{pmatrix}1 & 2 & 3\\\\ 4 & 5 & 6\\\\ 7 & 8 & 9\\end{pmatrix} $$\n", 336 | "\n", 337 | "$$ rotate\\_clockwise(A) = \\begin{pmatrix}7 & 4 & 1\\\\ 8 & 5 & 2\\\\ 9 & 6 & 3\\end{pmatrix} $$\n", 338 | "$$ rotate\\_counterclockwise(A) = \\begin{pmatrix}3 & 6 & 9\\\\ 2 & 5 & 8\\\\ 1 & 4 & 7\\end{pmatrix} $$\n", 339 | "$$ flip\\_horizontal(A) = \\begin{pmatrix}3 & 2 & 1\\\\ 6 & 5 & 4\\\\ 9 & 8 & 7\\end{pmatrix} $$\n", 340 | "$$ flip\\_vertical(A) = \\begin{pmatrix}7 & 8 & 9\\\\ 4 & 5 & 6\\\\ 1 & 2 & 3\\end{pmatrix} $$\n", 341 | "\n", 342 | "Hint 1: Всяка една от четирите функции може да се напише на един ред." 343 | ] 344 | }, 345 | { 346 | "cell_type": "code", 347 | "execution_count": 7, 348 | "metadata": {}, 349 | "outputs": [], 350 | "source": [ 351 | "# Use this for debugging, if needed\n", 352 | "def pretty_print(matrix):\n", 353 | " print('-' * (len(matrix[0]) * 2 - 1))\n", 354 | " for row in matrix:\n", 355 | " print(' '.join(str(item) for item in row))\n", 356 | " print('-' * (len(matrix[0]) * 2 - 1))\n", 357 | "\n", 358 | "\n", 359 | "def get_column(matrix, column):\n", 360 | " pass\n", 361 | "\n", 362 | "def rotate_clockwise(matrix):\n", 363 | " pass\n", 364 | "\n", 365 | "def rotate_counterclockwise(matrix):\n", 366 | " pass\n", 367 | "\n", 368 | "def flip_horizontal(matrix):\n", 369 | " pass\n", 370 | "\n", 371 | "def flip_vertical(matrix):\n", 372 | " pass" 373 | ] 374 | }, 375 | { 376 | "cell_type": "code", 377 | "execution_count": 8, 378 | "metadata": {}, 379 | "outputs": [ 380 | { 381 | "name": "stdout", 382 | "output_type": "stream", 383 | "text": [ 384 | "✅ All OK! +1.25 points\n" 385 | ] 386 | } 387 | ], 388 | "source": [ 389 | "matrix_1 = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]\n", 390 | "\n", 391 | "expected_clockwise_1 = [[7, 4, 1], [8, 5, 2], [9, 6, 3]]\n", 392 | "expected_counterclockwise_1 = [[3, 6, 9], [2, 5, 8], [1, 4, 7]]\n", 393 | "expected_flip_horizontal_1 = [[3, 2, 1], [6, 5, 4], [9, 8, 7]]\n", 394 | "expected_flip_vertical_1 = [[7, 8, 9], [4, 5, 6], [1, 2, 3]]\n", 395 | "\n", 396 | "assert rotate_clockwise(matrix_1) == expected_clockwise_1\n", 397 | "assert rotate_counterclockwise(matrix_1) == expected_counterclockwise_1\n", 398 | "assert flip_horizontal(matrix_1) == expected_flip_horizontal_1\n", 399 | "assert flip_vertical(matrix_1) == expected_flip_vertical_1\n", 400 | "\n", 401 | "\n", 402 | "matrix_2 = [[1, 2], [3, 4], [5, 6], [7, 8]]\n", 403 | "\n", 404 | "expected_clockwise_2 = [[7, 5, 3, 1], [8, 6, 4, 2]]\n", 405 | "expected_counterclockwise_2 = [[2, 4, 6, 8], [1, 3, 5, 7]]\n", 406 | "expected_flip_horizontal_2 = [[2, 1], [4, 3], [6, 5], [8, 7]]\n", 407 | "expected_flip_vertical_2 = [[7, 8], [5, 6], [3, 4], [1, 2]]\n", 408 | "\n", 409 | "assert rotate_clockwise(matrix_2) == expected_clockwise_2\n", 410 | "assert rotate_counterclockwise(matrix_2) == expected_counterclockwise_2\n", 411 | "assert flip_horizontal(matrix_2) == expected_flip_horizontal_2\n", 412 | "assert flip_vertical(matrix_2) == expected_flip_vertical_2\n", 413 | "\n", 414 | "\n", 415 | "matrix_3 = [[1, 2, 3], [4, 5, 6]]\n", 416 | "\n", 417 | "expected_clockwise_3 = [[4, 1], [5, 2], [6, 3]]\n", 418 | "expected_counterclockwise_3 = [[3, 6], [2, 5], [1, 4]]\n", 419 | "expected_flip_horizontal_3 = [[3, 2, 1], [6, 5, 4]]\n", 420 | "expected_flip_vertical_3 = [[4, 5, 6], [1, 2, 3]]\n", 421 | "\n", 422 | "assert rotate_clockwise(matrix_3) == expected_clockwise_3\n", 423 | "assert rotate_counterclockwise(matrix_3) == expected_counterclockwise_3\n", 424 | "assert flip_horizontal(matrix_3) == expected_flip_horizontal_3\n", 425 | "assert flip_vertical(matrix_3) == expected_flip_vertical_3\n", 426 | "\n", 427 | "print(\"✅ All OK! +1.25 points\")" 428 | ] 429 | }, 430 | { 431 | "cell_type": "markdown", 432 | "metadata": {}, 433 | "source": [ 434 | "## Задача 5 (1т.)" 435 | ] 436 | }, 437 | { 438 | "cell_type": "markdown", 439 | "metadata": {}, 440 | "source": [ 441 | "Напишете декоратор на име `sdrawkcab` със следното поведение:\n", 442 | "- Ако резултатът от функцията е от тип `str`, връща низът обърнат отзад-напред\n", 443 | "- Ако резултатът е от тип `list`:\n", 444 | " - За всеки елемент тип `str` в списъка, прилага горната трансформация.\n", 445 | " - Всички други елементи не се променят.\n", 446 | "- Ако резултатът от функцията не е `str`, връща резултата без промяна.\n", 447 | "\n", 448 | "*Hint: Можем да проверим дали променлива е от даден тип с помощта на `isinstance`*" 449 | ] 450 | }, 451 | { 452 | "cell_type": "code", 453 | "execution_count": 9, 454 | "metadata": {}, 455 | "outputs": [], 456 | "source": [ 457 | "def sdrawkcab(function):\n", 458 | " pass\n" 459 | ] 460 | }, 461 | { 462 | "cell_type": "code", 463 | "execution_count": 10, 464 | "metadata": {}, 465 | "outputs": [ 466 | { 467 | "name": "stdout", 468 | "output_type": "stream", 469 | "text": [ 470 | "✅ All OK! +1 points\n" 471 | ] 472 | } 473 | ], 474 | "source": [ 475 | "@sdrawkcab\n", 476 | "def my_string_function(name):\n", 477 | " return f\"Hello, {name}\"\n", 478 | "\n", 479 | "@sdrawkcab\n", 480 | "def my_non_string_function():\n", 481 | " return 42\n", 482 | "\n", 483 | "@sdrawkcab\n", 484 | "def list_of_strings():\n", 485 | " return [\"ab\", \"yaj\", \"yaj\"]\n", 486 | "\n", 487 | "@sdrawkcab\n", 488 | "def list_of_ints():\n", 489 | " return [15, 16]\n", 490 | "\n", 491 | "@sdrawkcab\n", 492 | "def mixed_list():\n", 493 | " return [15, 16, \"si\", \"a\", \"doog\", \"ecalp\", \"ot\", \"evah\", \"a\", \"reeb\", \".\"]\n", 494 | "\n", 495 | "expected_my_string_function_1 = \"obuyL ,olleH\"\n", 496 | "expected_my_non_string_function = 42\n", 497 | "expected_my_string_function_2 = \"backwards ,olleH\"\n", 498 | "expected_list_of_strings = [\"ba\", \"jay\", \"jay\"]\n", 499 | "expected_list_of_ints = [15, 16]\n", 500 | "expected_mixed_list = [15, 16, \"is\", \"a\", \"good\", \"place\", \"to\", \"have\", \"a\", \"beer\", \".\"]\n", 501 | "\n", 502 | "assert my_string_function(\"Lyubo\") == expected_my_string_function_1\n", 503 | "assert my_non_string_function() == expected_my_non_string_function\n", 504 | "assert my_string_function(\"sdrawkcab\") == expected_my_string_function_2\n", 505 | "assert list_of_strings() == expected_list_of_strings\n", 506 | "assert list_of_ints() == expected_list_of_ints\n", 507 | "assert mixed_list() == expected_mixed_list\n", 508 | "\n", 509 | "print(\"✅ All OK! +1 points\")" 510 | ] 511 | } 512 | ], 513 | "metadata": { 514 | "kernelspec": { 515 | "display_name": "Python 3", 516 | "language": "python", 517 | "name": "python3" 518 | }, 519 | "language_info": { 520 | "codemirror_mode": { 521 | "name": "ipython", 522 | "version": 3 523 | }, 524 | "file_extension": ".py", 525 | "mimetype": "text/x-python", 526 | "name": "python", 527 | "nbconvert_exporter": "python", 528 | "pygments_lexer": "ipython3", 529 | "version": "3.11.0rc1" 530 | }, 531 | "orig_nbformat": 4 532 | }, 533 | "nbformat": 4, 534 | "nbformat_minor": 2 535 | } 536 | --------------------------------------------------------------------------------