Skip to content
Open
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
3 changes: 3 additions & 0 deletions cursor-vs-copilot-python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Cursor vs Copilot: Which AI Editor Is Better for Python?

This folder provides the prompts used in the Real Python tutorial [Cursor vs Copilot: Which AI Editor Is Better for Python?](https://realpython.com/cursor-vs-copilot/)
17 changes: 17 additions & 0 deletions cursor-vs-copilot-python/cursor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.venv/
__pycache__/
*.pyc
*.egg-info/
build/
dist/
.pytest_cache/

notes.db
notes.db-journal
notes.db-wal
notes.db-shm
/notes/

# Editor / OS
.DS_Store
*.swp
55 changes: 55 additions & 0 deletions cursor-vs-copilot-python/cursor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Notes Manager

A simple command-line Markdown note manager. Each note is stored twice:

- as a **Markdown file** (with YAML frontmatter) in a notes directory — the canonical, human-editable copy.
- as a row in a **SQLite index** (`notes.db`) — used for fast search and listing.

## Install

```bash
pip install -e .
```

This installs the `notes` command (entry point defined in `pyproject.toml`).

## Usage

```bash
notes [--notes-dir DIR] [--db PATH] <command> [args]
```

`--notes-dir` (default `notes/`) and `--db` (default `notes.db`) let you point at a different notes store.

### Commands

| Command | Description |
|---|---|
| `notes add <title> [--tags a,b] [--body TEXT \| --body-file PATH]` | Create or update a note (body read from `--body`, `--body-file`, or stdin). |
| `notes get <title>` | Print a note by its exact title. |
| `notes list` | List all notes, most recently created first. |
| `notes search <query>` | Search notes whose title or body contains `query` (case-insensitive). |
| `notes list-tag <tag>` | List notes with a given tag (case-insensitive, exact tag match). |
| `notes reindex` | Rebuild the SQLite index from the Markdown files on disk (fixes drift if the index and files get out of sync). |

### Examples

```bash
notes add "Git Rebase vs Merge" --tags git,vcs --body "Rebase rewrites history; merge preserves it."
notes search rebase
notes list-tag git
notes get "Git Rebase vs Merge"
```

## Notes on storage

- Titles are unique; adding a note with an existing title updates (upserts) it.
- `search` and `list-tag` query the SQLite index only. If a Markdown file is added/edited outside the CLI (or the index gets out of sync), run `notes reindex`.
- The `notes/` directory and `notes.db` hold your actual note data and are gitignored — they aren't meant to be committed to source control.

## Development

```bash
pip install -e .
pytest
```
19 changes: 19 additions & 0 deletions cursor-vs-copilot-python/cursor/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.build_meta"

[project]
name = "notes-manager-cursor-test"
version = "0.1.0"
description = ""
requires-python = ">=3.10"
dependencies = [
"pyyaml==6.0.2",
"pytest==9.0.3",
]

[project.scripts]
notes = "notes_manager_cursor_test.cli:main"

[tool.setuptools.packages.find]
where = ["src"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""A command-line Markdown note manager."""

from .models import Note
from .store import NoteStore

__all__ = ["Note", "NoteStore"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .cli import main

if __name__ == "__main__":
raise SystemExit(main())
164 changes: 164 additions & 0 deletions cursor-vs-copilot-python/cursor/src/notes_manager_cursor_test/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"""Command-line interface for the Markdown note manager."""

from __future__ import annotations

import argparse
import sys
from collections.abc import Sequence
from pathlib import Path

from .models import Note
from .store import NoteStore

DEFAULT_NOTES_DIR = Path("notes")
DEFAULT_DB_PATH = Path("notes.db")


def _parse_tags(raw: str | None) -> list[str]:
if not raw:
return []
return [tag.strip() for tag in raw.split(",") if tag.strip()]


def _read_body(args: argparse.Namespace) -> str:
if args.body_file:
return Path(args.body_file).read_text(encoding="utf-8")
if args.body is not None:
return args.body
return sys.stdin.read()


def _print_note(note: Note) -> None:
tags = ", ".join(note.tags) if note.tags else "-"
print(f"# {note.title}")
print(f"tags: {tags}")
print(f"created_at: {note.created_at.isoformat()}")
print()
print(note.body)


def _print_note_summary(note: Note) -> None:
tags = ", ".join(note.tags) if note.tags else "-"
print(f"{note.title}\t[{tags}]\t{note.created_at.isoformat()}")


def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="notes",
description="A command-line Markdown note manager.",
)
parser.add_argument(
"--notes-dir",
default=DEFAULT_NOTES_DIR,
type=Path,
help=f"Directory to store Markdown note files in (default: {DEFAULT_NOTES_DIR})",
)
parser.add_argument(
"--db",
default=DEFAULT_DB_PATH,
type=Path,
help=f"Path to the SQLite index database (default: {DEFAULT_DB_PATH})",
)

subparsers = parser.add_subparsers(dest="command", required=True)

add_parser = subparsers.add_parser("add", help="Add a new note")
add_parser.add_argument("title", help="Title of the note")
add_parser.add_argument(
"--tags", default="", help="Comma-separated list of tags"
)
body_group = add_parser.add_mutually_exclusive_group()
body_group.add_argument("--body", help="Body text of the note")
body_group.add_argument(
"--body-file", help="Path to a file containing the note body"
)

search_parser = subparsers.add_parser(
"search", help="Search notes by title or body content"
)
search_parser.add_argument("query", help="Text to search for")

list_tag_parser = subparsers.add_parser(
"list-tag", help="List notes that have a given tag"
)
list_tag_parser.add_argument("tag", help="Tag to filter by")

get_parser = subparsers.add_parser(
"get", help="Retrieve a note by its exact title"
)
get_parser.add_argument("title", help="Title of the note")

subparsers.add_parser("list", help="List all notes")

subparsers.add_parser(
"reindex",
help="Rebuild the SQLite search index from the Markdown files on disk",
)

return parser


def main(argv: Sequence[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)

with NoteStore(args.notes_dir, args.db) as store:
if args.command == "add":
note = Note(
title=args.title,
body=_read_body(args),
tags=_parse_tags(args.tags),
)
store.add_note(note)
print(f"Added note '{note.title}'")
return 0

if args.command == "search":
results = store.find_notes_by_title_and_body(args.query)
if not results:
print("No notes found.")
return 0
for note in results:
_print_note_summary(note)
return 0

if args.command == "list-tag":
results = store.find_notes_by_tag(args.tag)
if not results:
print(f"No notes found with tag '{args.tag}'.")
return 0
for note in results:
_print_note_summary(note)
return 0

if args.command == "get":
note = store.find_note_by_title(args.title)
if note is None:
print(
f"No note found with title '{args.title}'.",
file=sys.stderr,
)
return 1
_print_note(note)
return 0

if args.command == "list":
results = store.find_all()
if not results:
print("No notes found.")
return 0
for note in results:
_print_note_summary(note)
return 0

if args.command == "reindex":
count = store.reindex()
print(f"Reindexed {count} note(s) from '{args.notes_dir}'.")
return 0

parser.error(f"Unknown command: {args.command}")
return 2


if __name__ == "__main__":
raise SystemExit(main())
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Read and write notes as Markdown files with YAML frontmatter.

The on-disk format looks like::

---
title: My Note
tags:
- foo
- bar
---
The body of the note goes here.
"""

from __future__ import annotations

import re
from pathlib import Path

import yaml

from .models import Note

_FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?\n)---\s*\n?(.*)\Z", re.DOTALL)


def slugify(title: str) -> str:
"""Turn a note title into a filesystem-friendly slug."""
slug = re.sub(r"[^a-zA-Z0-9]+", "-", title.strip().lower()).strip("-")
return slug or "note"


def serialize_note(note: Note) -> str:
"""Render a Note as Markdown text with YAML frontmatter."""
frontmatter = yaml.safe_dump(
{"title": note.title, "tags": list(note.tags)},
sort_keys=False,
)
return f"---\n{frontmatter}---\n{note.body}"


def deserialize_note(text: str, *, created_at=None) -> Note:
"""Parse Markdown text with YAML frontmatter into a Note.

``created_at`` is not stored in the frontmatter (only title and tags
are), so it must be supplied by the caller (e.g. from a database
record or the file's modification time). If omitted, the Note's
default (the current time) is used.
"""
match = _FRONTMATTER_RE.match(text)
if not match:
raise ValueError("Note text is missing YAML frontmatter")

raw_frontmatter, body = match.groups()
metadata = yaml.safe_load(raw_frontmatter) or {}

title = metadata.get("title", "")
tags = list(metadata.get("tags") or [])
body = body.lstrip("\n")

kwargs = {"title": title, "body": body, "tags": tags}
if created_at is not None:
kwargs["created_at"] = created_at
return Note(**kwargs)


def write_note_file(note: Note, directory: Path) -> Path:
"""Write ``note`` to a Markdown file inside ``directory`` and return its path."""
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{slugify(note.title)}.md"
path.write_text(serialize_note(note), encoding="utf-8")
return path


def read_note_file(path: Path, *, created_at=None) -> Note:
"""Read a Note from a Markdown file on disk."""
return deserialize_note(
path.read_text(encoding="utf-8"), created_at=created_at
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Data model for notes."""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime, timezone


@dataclass
class Note:
"""A single Markdown note."""

title: str
body: str
tags: list[str] = field(default_factory=list)
created_at: datetime = field(
default_factory=lambda: datetime.now(timezone.utc)
)
updated_at: datetime = field(
default_factory=lambda: datetime.now(timezone.utc)
)
is_archived: bool = False
Loading
Loading