Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions pl-001-python-basics-task/.github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
pull_request:
types: [opened, synchronize, reopened]

permissions:
contents: read

jobs:
checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true

- name: Install dependencies
run: uv sync --locked

- name: Ruff lint
run: uv run ruff check src

- name: Ruff format check
run: uv run ruff format --check src

- name: MyPy
run: uv run mypy --strict src
52 changes: 52 additions & 0 deletions pl-001-python-basics-task/.github/workflows/label-by-group.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Label PR by group

on:
pull_request_target:
types: [opened, edited, reopened]

permissions:
pull-requests: write

jobs:
label-selection:
runs-on: ubuntu-latest
steps:
- name: Set label from PR title
uses: actions/github-script@v7
with:
script: |
const title = context.payload.pull_request.title;

// Expected title format: "<hw number>. <full name>. <group number>"
const parts = title.split(".").map((part) => part.trim());

if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
console.log(`Title "${title}" does not match the expected template, skipping.`);
return;
}

const [hwNumber, , groupNumber] = parts;

if (!/^\d+$/.test(hwNumber)) {
console.log(`Title "${title}" does not match the expected template, skipping.`);
return;
}

const { data: labels } = await github.rest.issues.listLabelsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
});

const matchingLabel = labels.find((label) => label.name === groupNumber);

if (!matchingLabel) {
console.log(`No label named "${groupNumber}" found, skipping.`);
return;
}

await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: [matchingLabel.name],
});
13 changes: 13 additions & 0 deletions pl-001-python-basics-task/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Byte-compiled
__pycache__/
*.py[cod]

# Virtual environments
.venv/

# Linter / type-checker caches
.ruff_cache/
.mypy_cache/

# Claude
.claude
1 change: 1 addition & 0 deletions pl-001-python-basics-task/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
108 changes: 108 additions & 0 deletions pl-001-python-basics-task/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Практическое задание по курсу PL-001

В рамках задания мы напишем на Python игрушечный бэкенд для интернет-магазина с локальным хранилищем данных в памяти: научимся создавать, читать, изменять и удалять товары, работать с корзиной и управлять всем этим через простую консоль администратора. Задание выполняется поэтапно, и каждый следующий шаг опирается на предыдущий.

## Мануалы

- [Запуск локальных проверок](manuals/local_checks.md) — как установить uv на macOS и Windows и запустить скрипты, которые локально выполняют те же проверки кода, что и CI на GitHub.
- [Сдача домашнего задания](manuals/submit_homework.md) — как загрузить решение в свой форк, оформить Pull Request в исходный репозиторий и что нужно для того, чтобы работа была принята на проверку.

## Задания

- [Часть 1. Генератор идентификаторов Twitter Snowflake](src/part1/TASK.md) — нужно написать генератор 64-битных числовых идентификаторов, в которые упакованы метка времени, номер узла и счётчик последовательности. Такие идентификаторы уникальны и примерно упорядочены по времени, а из самого идентификатора можно узнать, когда и на каком узле он был создан.
- [Часть 2. CRUD и корзина для интернет-магазина](src/part2/TASK.md) — нужно реализовать хранилище товаров в памяти (список кортежей), операции создания, чтения, изменения и удаления записей, аккуратную работу с ценами через `Decimal` и корзину покупателя, в которую товары переносятся со склада и обратно без потери количества.

## Критерии оценивания

Максимальный балл за каждое задание — **10**. Правильность решения проверяется закрытыми тестами.

### Условия принятия работы

Работа принимается к оцениванию, только если выполнены **все** условия:

- код проходит проверки [ruff](https://docs.astral.sh/ruff/) и [MyPy](https://mypy-lang.org/) (запустить их можно [локально](manuals/local_checks.md));
- работа сдана до дедлайна.

Если хотя бы одно условие не выполнено, работа не принимается и оценивается в 0 баллов.

### Как считается балл за правильность

Тесты разбиты на функциональные блоки: каждый блок соответствует функции или классу из задания и имеет свой вес (более сложные блоки весят больше). Внутри блока тесты делятся на две группы:

| Группа тестов | Доля веса блока | Что проверяет |
|---|---|---|
| Базовые (типичные входные данные) | 70% | Основная логика реализована верно |
| Граничные (пустой ввод, нулевые и предельные значения, повторы и т. п.) | 30% | Обработка особых случаев |

Формулы:

```
балл_блока = 0.7 · доля_пройденных_базовых + 0.3 · доля_пройденных_граничных
балл_за_правильность = 10 · Σ(вес_блока · балл_блока) / Σ(вес_блока)
```

Дополнительные правила:

- **Нереализованный функционал.** Блок, в котором функционал не реализован (заглушка, `pass`, `NotImplementedError`), получает 0 баллов. Остальные блоки оцениваются как обычно, дополнительного штрафа за нереализованное нет.
- **Ошибки выполнения.** Тест, в котором возникло необработанное исключение, считается проваленным.
- **Зависание.** У каждого теста есть ограничение по времени выполнения. Если решение не укладывается в него (например, из-за бесконечного цикла), тест считается проваленным.

### Штрафы за оформление

Кроме правильности, оценивается качество кода. За нарушения ниже из балла за правильность вычитаются штрафы. Каждое нарушение штрафуется **один раз**, сколько бы раз оно ни встретилось в работе.

У каждого нарушения есть уникальный код: `S` — серьёзные, `M` — средние, `T` — типовые. При проверке преподаватель указывает в комментарии к работе только коды (например, `S1, M2, T1`), а расшифровку и штраф можно найти в таблицах ниже.

#### Серьёзные нарушения (−1 балл за каждое)

| Код | Нарушение | Что имеется в виду |
|---|---|---|
| `S1` | Копипаста | Вместо вынесения повторяющейся логики в функцию, метод или цикл один и тот же (или почти тот же) код скопирован несколько раз. Мешает расширяемости: любое изменение приходится вносить во все копии |
| `S2` | Слишком длинные функции | Функция делает много разных вещей одновременно (вычисления, проверки, преобразования, форматирование в одном месте) и не читается целиком. Ориентир: более 30–40 строк или несколько несвязанных обязанностей. Такой код тяжело читать и поддерживать |
| `S3` | Магические значения | Числа и строки с неочевидным смыслом прописаны прямо в коде, а не вынесены в именованные константы. Как и копипаста, мешает расширяемости: значение приходится искать и менять по всему коду |
| `S4` | Слишком широкий перехват исключений | Голый `except:` или `except Exception:` без необходимости; молча проглоченные ошибки (`except ...: pass`). Так можно пропустить что-то важное |
| `S5` | Изменение входных данных | Функция неожиданно изменяет переданные ей аргументы, хотя из названия и описания это не следует. Неожиданное поведение, источник серьёзных ошибок |

#### Средние нарушения (−0.5 балла за каждое)

| Код | Нарушение | Что имеется в виду |
|---|---|---|
| `M1` | Большая вложенность | Более трёх уровней вложенности (`if`, `for`, `while`, `try`, `with`) внутри одной функции. Лечится ранним возвратом (`return`, `continue`) и выносом логики в отдельные функции |
| `M2` | Неинформативные имена | Имена вроде `a`, `tmp`, `data2`, `x1`, а также имена, не отражающие суть, или не соответствующие PEP 8 (например, `camelCase` для функций и переменных) |
| `M3` | Избыточные глобальные переменные | Изменяемое глобальное состояние там, где достаточно параметров, возвращаемых значений или атрибутов класса |
| `M4` | Мёртвый код | Код, который нигде не используется: неиспользуемые функции, классы, переменные, константы, импорты, недостижимые ветки. Сюда же относятся крупные куски закомментированного кода «на будущее» и оставленный отладочный `print`. Затрудняет чтение, но на работу программы не влияет |

#### Типовые нарушения хорошего тона (−0.25 балла за каждое)

| Код | Нарушение |
|---|---|
| `T1` | `else` или `elif` после `return`, `raise`, `continue` или `break` |
| `T2` | Явное приведение к `bool` в условиях (`if bool(x):`) и сравнение с `True` / `False` (`if x == True:`) |
| `T3` | Сравнение с синглтонами и членами enum'ов через `==` / `!=` вместо `is` / `is not` |
| `T4` | Проверка пустоты коллекции через `len(x) == 0` вместо `if not x` |
| `T5` | Обход через `for i in range(len(items))`, когда индекс не нужен (вместо `for item in items` или `enumerate`) |
| `T6` | `if cond: return True else: return False` вместо `return cond` |
| `T7` | Изменяемое значение по умолчанию у аргумента (`def f(x=[])`) |
| `T8` | Ручная конкатенация строк вместо f-строк |
| `T9` | Избыточные комментарии, пересказывающие очевидный код |

### Штраф за списывание

Если работа подозревается в списывании, студент получает **−5 баллов**. Эти баллы не вычитаются из оценки за правильность, а **заменяют** итоговую оценку: итоговой оценкой становится −5, независимо от результата тестов.

Студент может оспорить этот балл на занятии, доказав, что не списывал, а написал код самостоятельно (например, объяснив решение и ответив на вопросы по нему). Если подозрение снимается, работа оценивается по обычным критериям.

### Итоговая оценка

```
если есть подозрение в списывании:
итоговая_оценка = −5
иначе:
итоговая_оценка = max(0, балл_за_правильность − штрафы_за_оформление)
```

Итоговая оценка за домашнее задание округляется до ближайшего целого по правилам математики: дробная часть от 0.5 и выше округляется вверх, меньше 0.5 — вниз (например, 7.5 → 8, 7.4 → 7).

### Как оспорить оценку

Если вы не согласны с оценкой, напишите преподавателю. Укажите, с каким блоком или с какой частью критериев вы не согласны, и приведите аргументы. Оценка пересматривается по тем же критериям, которые описаны выше.
90 changes: 90 additions & 0 deletions pl-001-python-basics-task/manuals/local_checks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Запуск локальных проверок

Перед тем как открыть Pull Request, прогоните у себя те же проверки, что запускаются в CI на GitHub. Так вы найдёте ошибки заранее, не дожидаясь результата CI.

## Что проверяется

Скрипты устанавливают зависимости, затем проверяют код в папке `src`: линтером, форматированием и статическим анализатором типов. Если какая-то проверка не пройдена, скрипт останавливается с ошибкой.

## Установка uv

[uv](https://docs.astral.sh/uv/) — менеджер пакетов и окружений для Python. Проверки запускаются через него, поэтому вам потребуется его установить.

### macOS

Через официальный установщик:

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

Или через Homebrew:

```bash
brew install uv
```

### Windows

Через PowerShell:

```powershell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```

Или через winget:

```powershell
winget install --id=astral-sh.uv -e
```

### Проверка установки

Закройте и снова откройте терминал (иначе он не увидит обновлённый `PATH`), затем выполните:

```bash
uv --version
```

Если команда напечатала версию, всё готово. Другие способы установки описаны в [документации uv](https://docs.astral.sh/uv/getting-started/installation/).

## Запуск проверок

Скрипты лежат в корне репозитория. Их можно запускать из любой папки, они сами переходят в корень проекта. Если `uv` не установлен, скрипт напечатает сообщение об этом и завершится.

### macOS / Linux

```bash
./run_local_checks
```

Если появилась ошибка `Permission denied`, сделайте скрипт исполняемым и запустите снова:

```bash
chmod +x run_local_checks
./run_local_checks
```

### Windows

В PowerShell или в командной строке (cmd):

```powershell
.\run_local_checks.bat
```

Если у вас установлен Git Bash, можно запустить и bash-версию:

```bash
./run_local_checks
```

## Как читать результат

Скрипт печатает заголовок каждого шага (`==> Ruff lint` и так далее). При успехе в конце выводится `All checks passed.`

Если какой-то шаг упал, исправьте замечания из его вывода и запустите скрипт снова. Форматирование можно исправить автоматически:

```bash
uv run ruff format src
```
Loading
Loading