Metadata-Version: 2.4
Name: dmytrokrytovychneoversity-personal-assistant
Version: 1.0.0
Summary: Personal Assistant — a command line contact book and note manager
Author-email: Dmytro Krytovych <dmytro.krytovych.neoversity@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/dmytrokrytovychneoversity/goit-pycore-personal-assistant
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: prompt-toolkit>=3.0
Requires-Dist: rich>=13.0
Requires-Dist: textual>=1.0
Provides-Extra: postgres
Requires-Dist: psycopg[binary]>=3.1; extra == "postgres"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Requires-Dist: ruff>=0.12; extra == "dev"
Requires-Dist: import-linter>=2.3; extra == "dev"
Requires-Dist: mypy>=1.11; extra == "dev"
Requires-Dist: pyftpdlib>=1.5; extra == "dev"
Requires-Dist: psycopg[binary]>=3.1; extra == "dev"
Dynamic: license-file

# Personal Assistant

Final project for the GoIT Neoversity course _Python Programming: Foundations and Best Practices_.

**Personal Assistant** is a command line application that manages a contact book and text
notes. It can be used three ways — an interactive shell, single commands from your own
shell, and a full-screen dashboard — and it keeps its data in whichever store you point
it at: a local file, SQLite, Postgres, or an FTP server.

## Features

**Contacts** — a name, any number of phone numbers, and an optional email, address and
birthday. Add, edit, delete, list, search across every field, and ask who has a birthday
in the next N days.

**Notes** — a title, the text, and any number of tags. Add, edit, delete, list, search
across every field, filter by tag, and sort by tag.

**Validation** — every field validates itself, so an invalid phone number, email or date
cannot be stored no matter which of the three frontends it was typed into. A refused
value is reported and re-asked; it never ends the program.

**Saving** — every change is written straight away, so nothing is lost if the program is
closed abruptly. Local files are written to a temporary file and renamed over the
target, so an interrupted save leaves the previous version readable.

## Installation

Requires **Python 3.11 or newer**.

```bash
pip install dmytrokrytovychneoversity-personal-assistant
```

This installs the `assistant` command, which works from anywhere on the system.

Or from a clone:

```bash
git clone https://github.com/dmytrokrytovychneoversity/goit-pycore-final.git
cd goit-pycore-final
python -m venv .venv && source .venv/bin/activate
pip install -e .
```

To use Postgres for storage, install the optional driver as well:

```bash
pip install "dmytrokrytovychneoversity-personal-assistant[postgres]"
```

## Getting started

Run it with no arguments to open the interactive shell:

```bash
assistant
```

```
Personal Assistant — 0 contacts and 0 notes on board. Type 'help' to begin.
assistant ❯ add-contact "John Doe" 0670000111 --email john@example.com
Added contact 'John Doe'.
```

Type `help` for every command, or `help add-contact` for one command's details.
`exit`, `quit`, `close` or **Ctrl+D** leaves.

## The three ways to use it

### 1. The interactive shell — `assistant`

The main way to use the assistant. It offers:

|                       |                                                                                                                                                                                                                                                         |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Tab completion**    | Completes command names, option names, and live values — the names of your contacts, the titles of your notes, the tags you have used, the phone numbers of the contact already named on the line.                                                      |
| **Forms**             | Anything you leave out is asked for. Typing `add-contact` on its own walks you through every field; typing `add-contact "John Doe"` asks only for what is still missing. Each answer is validated as you enter it, and a bad one is simply asked again. |
| **Hotkeys**           | **F1** help · **F2** new contact · **F3** new note · **Ctrl+C** cancel the current line or form · **Ctrl+D** exit                                                                                                                                       |
| **History**           | Previous commands are kept between sessions; **↑** and **↓** walk through them.                                                                                                                                                                         |
| **Confirmation**      | Deleting anything asks first.                                                                                                                                                                                                                           |
| **Birthday reminder** | Whose birthday falls in the next 7 days is shown when you start.                                                                                                                                                                                        |

Values containing spaces go in quotes: `delete-contact "John Doe"`.

### 2. One command at a time — `assistant cli <command>`

The same commands, run straight from your own shell, which makes them scriptable:

```bash
assistant cli add-contact "John Doe" 0670000111 --email john@example.com
assistant cli find-contact john
assistant cli --yes delete-contact "John Doe"
```

There is nobody to ask anything here, so a missing argument is an error rather than a
prompt, and a command that deletes something needs `--yes`. The exit code is **0** when
the command worked, **1** when it failed, and **2** when the invocation itself was wrong.

`assistant cli --help` lists every command, and `assistant cli <command> --help` explains
one of them.

With `--renderer json` every command prints one JSON envelope, which is what to use when
another program is reading the output:

```bash
assistant cli --renderer json find-contact john
```

```json
{
  "ok": true,
  "message": "1 contact matching 'john'.",
  "contacts": [
    {
      "name": "John Doe",
      "phones": ["+380670000111"],
      "email": "john@example.com",
      "address": null,
      "birthday": "24.08.1991"
    }
  ],
  "notes": []
}
```

A failure keeps the same shape: `{"ok": false, "error": {"code": "not_found", "message": "…"}}`.

### 3. The dashboard — `assistant dashboard`

A full-screen terminal UI with a tab for contacts and a tab for notes, each with a live
search box over a table.

Everything is reachable from the keyboard; the mouse is never needed.

| Key       |                                                       |
| --------- | ----------------------------------------------------- |
| **1**     | the contacts tab                                      |
| **2**     | the notes tab                                         |
| **a**     | add                                                   |
| **e**     | edit the highlighted row                              |
| **d**     | delete the highlighted row (asks first)               |
| **/**     | jump to the search box                                |
| **↑ ↓**   | move through the list; up from the first row searches |
| **Esc**   | leave the search box, keeping what you typed          |
| **q**     | quit                                                  |

In the add, edit and delete dialogs: **↑ ↓** (**← →** for the buttons) move,
**Enter** accepts, **Esc** cancels. Each dialog states those keys at the bottom.
Inside the note text, up and down move within the text — use **Tab** to leave it.

## Commands

Every command below works in all three frontends. Arguments can be given in order
(`add-phone "John Doe" 0670000111`) or by name (`add-phone --name "John Doe" --phone
0670000111`), and anything left out is asked for in the interactive shell.

### Contacts

| Command                                                                                          | What it does                                                     |
| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| `add-contact <name> [phone ...] [--email <email>] [--address <address>] [--birthday <birthday>]` | Add a contact. Repeat the phone to give several.                 |
| `edit-contact <name> <field> <value>`                                                            | Change one field: `name`, `email`, `address` or `birthday`.      |
| `delete-contact <name>`                                                                          | Delete a contact.                                                |
| `add-phone <name> <phone>`                                                                       | Add a phone number to a contact.                                 |
| `remove-phone <name> <phone>`                                                                    | Remove a phone number from a contact.                            |
| `contacts`                                                                                       | Show every contact. Also `all-contacts`.                         |
| `find-contact <query>`                                                                           | Search name, phones, email, address and birthday. Also `search`. |
| `birthdays [days]`                                                                               | Who has a birthday in the next `days` (7 by default).            |

### Notes

| Command                             | What it does                                        |
| ----------------------------------- | --------------------------------------------------- |
| `add-note <title> <text> [tag ...]` | Add a note. Repeat the tag to give several.         |
| `edit-note <title> <field> <value>` | Change one field: `title` or `text`.                |
| `delete-note <title>`               | Delete a note.                                      |
| `notes`                             | Show every note. Also `all-notes`.                  |
| `find-note <query>`                 | Search title, text and tags.                        |
| `add-tag <title> <tag>`             | Add a tag to a note.                                |
| `remove-tag <title> <tag>`          | Remove a tag from a note.                           |
| `find-tag <tag>`                    | Show every note carrying a tag.                     |
| `sort-notes`                        | Show every note ordered by tag, untagged ones last. |

### What each field accepts

| Field       | Rule                                                                                                                                                    |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Name, title | At least 2 characters.                                                                                                                                  |
| Phone       | A Ukrainian number, stored as `+380XXXXXXXXX`. `0670000111`, `380670000111`, `+380670000111` and `+38 (067) 000-01-11` are all read as the same number. |
| Email       | `user@example.com`.                                                                                                                                     |
| Birthday    | `DD.MM.YYYY`, e.g. `24.08.1991`.                                                                                                                        |
| Address     | One line of free text.                                                                                                                                  |
| Tag         | One word of letters, digits, `-` or `_`. A leading `#` is accepted and dropped, and tags are lowercased, so `#Work`, `work` and `WORK` are one tag.     |

## Where the data is kept

By default, JSON files in `~/.assistant`. Both the folder and the storage can be
changed, either with an option or with an environment variable:

```bash
assistant --storage sqlite --data-dir ~/my-data
```

```bash
export ASSISTANT_STORAGE=sqlite
export ASSISTANT_DATA_DIR=~/my-data
assistant
```

| `--storage`              | Where the data goes                                                                 |
| ------------------------ | ----------------------------------------------------------------------------------- |
| `json` _(default)_       | `contacts.json` and `notes.json` in the data folder — readable and editable by hand |
| `pickle`                 | the same documents in Python's binary pickle format                                 |
| `sqlite`                 | `assistant.db` in the data folder                                                   |
| `postgres`               | a PostgreSQL database, from `ASSISTANT_POSTGRES_DSN`                                |
| `ftp-json`, `ftp-pickle` | the documents on an FTP server, from `ASSISTANT_FTP_URL`                            |
| `memory`                 | nowhere — forgotten when the program exits, useful for trying things out            |

| Environment variable     |                                                                                      |
| ------------------------ | ------------------------------------------------------------------------------------ |
| `ASSISTANT_STORAGE`      | which of the above to use                                                            |
| `ASSISTANT_DATA_DIR`     | the folder for local data (default `~/.assistant`)                                   |
| `ASSISTANT_POSTGRES_DSN` | e.g. `postgresql://user:password@localhost:5432/assistant`                           |
| `ASSISTANT_FTP_URL`      | e.g. `ftp://user:password@host/assistant` — the document files are created inside it |

The stores are interchangeable because they all satisfy the same interface; the whole
application above them is unchanged whichever one is chosen.

> **A note on `pickle`:** unpickling runs whatever the file says, so a pickle document is
> only ever as trustworthy as wherever it came from. `json` is the safer default.

## Options

Where the data lives is the only thing all three modes need, so those options are
global and come **before** the mode:

| Global option       |                                |
| ------------------- | ------------------------------ |
| `--storage <kind>`  | which store to use (see above) |
| `--data-dir <path>` | the folder for local data      |
| `--version`         | print the version              |
| `-h`, `--help`      | print help                     |

```bash
assistant --storage sqlite cli contacts
```

The rest only mean something when one command is run and read by something else, so
they belong to `cli` and come **after** it:

| `cli` option        |                                                          |
| ------------------- | -------------------------------------------------------- |
| `--renderer <kind>` | `rich` (default), `plain` for pipes, `json` for programs |
| `-y`, `--yes`       | confirm destructive commands without asking              |
| `-h`, `--help`      | the command list, or the named command's help            |

`-y`/`--yes` and `-h`/`--help` may also come after the command, where they often read
better: `assistant cli delete-contact "John Doe" -y`.

```bash
assistant cli --renderer json contacts
```

## Development

```bash
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
```

| Command                 | Checks                                         |
| ----------------------- | ---------------------------------------------- |
| `ruff check .`          | style and import ordering                      |
| `ruff format --check .` | formatting                                     |
| `mypy`                  | types, `strict`, over `src`                    |
| `lint-imports`          | the layer rules the architecture depends on    |
| `pytest`                | `tests/unit`, `tests/integration`, `tests/e2e` |

CI runs all five on every push and pull request; the tests run on Python 3.11–3.13.

The Postgres tests are skipped unless `ASSISTANT_TEST_POSTGRES_DSN` points at a database
that may be wiped — CI provides one. To run them locally:

```bash
docker run --rm -d -p 5432:5432 \
  -e POSTGRES_USER=assistant -e POSTGRES_PASSWORD=assistant -e POSTGRES_DB=assistant \
  --name assistant-pg postgres:16

ASSISTANT_TEST_POSTGRES_DSN=postgresql://assistant:assistant@localhost:5432/assistant pytest
```

## Releasing

1. Bump `version` in `pyproject.toml`.
2. Publish a GitHub release tagged `v<version>` (the tag must match, or the build fails).
3. `release.yml` builds, checks and uploads to PyPI via Trusted Publishing.

## License

MIT
