Case Study 2: Maya Organizes Her Project Folder
Background
Maya Reyes has been freelancing for three years. Her project folder on her MacBook has grown from a tidy system she designed on day one into something she is no longer proud of. There are completed client folders sitting at the top level alongside active work. Draft deliverables have names like proposal_v3_FINAL_actualfinal.docx. Some client folders contain files named without the client name at all, making search results useless.
She spends fifteen minutes on average at the start of each work session just orienting herself — finding the right version of the right file for the right client. Over a month, that is nearly two hours of dead time.
Her goal: a single script she can run each Monday morning that:
1. Moves completed project folders to an archived/ directory
2. Renames key client files to include the client name and date
3. Creates a clean folder structure for any new projects she lists
Maya's Project Structure (Before)
~/consulting/
acme_corp/
proposal_v1.docx
proposal_v2.docx
proposal_FINAL.docx
kickoff_notes.txt
acme_statement_of_work.pdf
invoices/
invoice_001.pdf
invoice_002.pdf
hartmann_logistics/
scope_doc.docx
hartmann_proposal_signed.pdf
status: COMPLETED
riverside_dental/
initial_call_notes.txt
[status: ACTIVE]
belmont_realty/
[no files yet — just created]
archived/
old_client_2022/
The hartmann_logistics folder is complete but still sits at the top level. The acme_corp folder has multiple versions of the proposal with unhelpful names. The belmont_realty folder was just created and needs the standard subdirectory structure.
The Script
"""
maya_project_organizer.py
Weekly project folder organization for Maya Reyes Consulting.
Operations:
1. Move completed project folders to archived/
2. Rename key client files to include client name + date prefix
3. Create standard folder structure for new/empty project folders
Usage:
python maya_project_organizer.py
python maya_project_organizer.py --dry-run
python maya_project_organizer.py --archive-completed --rename-files --setup-new
"""
import argparse
import datetime
import logging
import re
import shutil
import sys
from pathlib import Path
# ── CONFIGURATION ─────────────────────────────────────────────────────────────
CONSULTING_DIR = Path.home() / "consulting"
ARCHIVE_DIR = CONSULTING_DIR / "archived"
# A project folder is considered "completed" if it contains a file named
# 'status.txt' whose content is "COMPLETED" (case-insensitive).
# This is Maya's simple convention — she creates a status.txt file when
# she closes a project.
STATUS_FILENAME = "status.txt"
COMPLETED_STATUS = "completed"
# File types that are "key client deliverables" — these get renamed
# to include the client name and a date prefix.
DELIVERABLE_EXTENSIONS = {".docx", ".pdf", ".xlsx", ".pptx"}
# Folder structure to create for new projects
NEW_PROJECT_TEMPLATE = {
"deliverables": {}, # Final versions sent to clients
"drafts": {}, # Working documents
"client_materials": {}, # Things the client sends to Maya
"invoices": {}, # Billing
"notes": {}, # Call notes, meeting notes
}
# ── LOGGING ───────────────────────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(message)s",
datefmt="%H:%M:%S",
handlers=[logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger("maya_organizer")
# ── STATUS DETECTION ──────────────────────────────────────────────────────────
def get_project_status(project_dir: Path) -> str:
"""
Read the project status from status.txt.
Returns "active" if no status file is found.
Returns the content of status.txt (lowercased and stripped) otherwise.
"""
status_file = project_dir / STATUS_FILENAME
if not status_file.exists():
return "active"
return status_file.read_text(encoding="utf-8").strip().lower()
def is_project_folder(path: Path) -> bool:
"""
Return True if path looks like a project folder (not a system folder).
Excludes the archive directory itself and hidden directories.
"""
if not path.is_dir():
return False
if path.name.startswith("."):
return False
if path.resolve() == ARCHIVE_DIR.resolve():
return False
return True
def is_empty_project_folder(project_dir: Path) -> bool:
"""
Return True if a project folder contains no deliverable files.
A folder with only .gitkeep or status.txt files is considered empty.
"""
for item in project_dir.rglob("*"):
if item.is_file():
if item.name not in {".gitkeep", STATUS_FILENAME, ".DS_Store"}:
return False
return True
# ── STEP 1: ARCHIVE COMPLETED PROJECTS ───────────────────────────────────────
def archive_completed_projects(
consulting_dir: Path,
archive_dir: Path,
logger: logging.Logger,
dry_run: bool = False,
) -> list[str]:
"""
Move project folders with COMPLETED status to the archive directory.
Returns a list of project names that were archived.
"""
archived = []
for project_dir in sorted(consulting_dir.iterdir()):
if not is_project_folder(project_dir):
continue
status = get_project_status(project_dir)
if status != COMPLETED_STATUS:
continue
destination = archive_dir / project_dir.name
# Handle the case where this project already exists in archive
if destination.exists():
timestamp = datetime.datetime.now().strftime("%Y%m%d")
destination = archive_dir / f"{project_dir.name}_{timestamp}"
logger.info(
f" Archive conflict: renaming to {destination.name}"
)
if not dry_run:
archive_dir.mkdir(parents=True, exist_ok=True)
shutil.move(str(project_dir), str(destination))
logger.info(f" Archived: {project_dir.name}/ -> archived/{destination.name}/")
else:
logger.info(
f" [DRY RUN] Would archive: {project_dir.name}/ -> archived/{destination.name}/"
)
archived.append(project_dir.name)
return archived
# ── STEP 2: RENAME CLIENT DELIVERABLES ────────────────────────────────────────
def build_client_filename(client_name: str, original_name: str) -> str:
"""
Build a standardized filename incorporating the client name and today's date.
The client_name comes from the project folder name (e.g., "acme_corp").
The original filename's extension is preserved.
Examples:
client_name="acme_corp", original_name="proposal_FINAL.docx"
-> "2024-01-15_acme_corp_proposal_final.docx"
"""
today = datetime.date.today().strftime("%Y-%m-%d")
stem, suffix = Path(original_name).stem, Path(original_name).suffix
# Standardize the stem: lowercase, spaces to underscores, strip specials
clean_stem = stem.lower()
clean_stem = re.sub(r"\s+", "_", clean_stem)
clean_stem = re.sub(r"[^a-z0-9_]", "_", clean_stem)
clean_stem = re.sub(r"_+", "_", clean_stem).strip("_")
# Avoid double-prefixing if client name is already in the filename
if client_name in clean_stem:
return f"{today}_{clean_stem}{suffix.lower()}"
else:
return f"{today}_{client_name}_{clean_stem}{suffix.lower()}"
def should_rename_file(file_path: Path, client_name: str) -> bool:
"""
Return True if a file should be renamed.
Skips files that:
- Are not in the DELIVERABLE_EXTENSIONS set
- Already have today's date prefix
- Have names that already look fully standardized
"""
if file_path.suffix.lower() not in DELIVERABLE_EXTENSIONS:
return False
today = datetime.date.today().strftime("%Y-%m-%d")
if file_path.name.startswith(today):
return False
return True
def rename_client_files(
consulting_dir: Path,
logger: logging.Logger,
dry_run: bool = False,
) -> list[dict]:
"""
Rename key deliverable files in active project folders to include
the client name and a date prefix.
Only renames files directly in project folders (not in subdirectories).
Returns a list of rename operation records.
"""
rename_records = []
for project_dir in sorted(consulting_dir.iterdir()):
if not is_project_folder(project_dir):
continue
status = get_project_status(project_dir)
if status == COMPLETED_STATUS:
continue # Completed projects are being archived, not renamed
client_name = project_dir.name # e.g., "acme_corp"
for file_path in sorted(project_dir.iterdir()):
if not file_path.is_file():
continue
if not should_rename_file(file_path, client_name):
continue
new_name = build_client_filename(client_name, file_path.name)
new_path = file_path.parent / new_name
record = {
"client": client_name,
"old_name": file_path.name,
"new_name": new_name,
"old_path": str(file_path),
"new_path": str(new_path),
"conflict": new_path.exists(),
}
if record["conflict"]:
logger.warning(
f" Skipping (conflict): {file_path.name} -> {new_name} (already exists)"
)
elif not dry_run:
file_path.rename(new_path)
logger.info(f" Renamed: [{client_name}] {file_path.name} -> {new_name}")
else:
logger.info(
f" [DRY RUN] Would rename: [{client_name}] {file_path.name} -> {new_name}"
)
rename_records.append(record)
return rename_records
# ── STEP 3: SET UP NEW PROJECT FOLDERS ───────────────────────────────────────
def setup_new_project_folders(
consulting_dir: Path,
template: dict,
logger: logging.Logger,
dry_run: bool = False,
) -> list[str]:
"""
Create standard subdirectory structure in project folders that are empty.
A folder is considered "new" if it has no deliverable files and has
no subdirectories yet.
Returns a list of project names that were set up.
"""
setup = []
for project_dir in sorted(consulting_dir.iterdir()):
if not is_project_folder(project_dir):
continue
# Only touch folders that look genuinely new (no subdirectories)
has_subdirs = any(p.is_dir() for p in project_dir.iterdir())
if has_subdirs:
continue
if not is_empty_project_folder(project_dir):
continue
# This looks like a newly created, empty project folder
logger.info(f" Setting up new project: {project_dir.name}/")
for subfolder_name in template.keys():
subfolder = project_dir / subfolder_name
if not dry_run:
subfolder.mkdir(exist_ok=True)
(subfolder / ".gitkeep").touch()
logger.info(f" {'[DRY RUN] ' if dry_run else ''}Created: {subfolder_name}/")
# Create a status file defaulting to "active"
status_file = project_dir / STATUS_FILENAME
if not status_file.exists() and not dry_run:
status_file.write_text("active\n", encoding="utf-8")
setup.append(project_dir.name)
return setup
# ── SUMMARY ────────────────────────────────────────────────────────────────────
def print_summary(
archived: list,
renamed: list,
setup: list,
dry_run: bool,
) -> None:
prefix = "[DRY RUN] " if dry_run else ""
print("\n" + "=" * 60)
print(f"{prefix}WEEKLY PROJECT ORGANIZER — SUMMARY")
print("=" * 60)
print(f" Completed projects archived: {len(archived)}")
for name in archived:
print(f" - {name}")
successful_renames = [r for r in renamed if not r["conflict"]]
print(f"\n Files renamed: {len(successful_renames)}")
for r in successful_renames:
print(f" [{r['client']}] {r['old_name']}")
print(f" -> {r['new_name']}")
print(f"\n New projects initialized: {len(setup)}")
for name in setup:
print(f" - {name}")
print("=" * 60)
if dry_run:
print("Run without --dry-run to apply changes.\n")
else:
print("Done.\n")
# ── MAIN ──────────────────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(
description="Maya Reyes Consulting — weekly project folder organizer"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show planned actions without changing any files",
)
parser.add_argument(
"--archive-completed",
action="store_true",
default=True,
help="Archive project folders with COMPLETED status (default: on)",
)
parser.add_argument(
"--rename-files",
action="store_true",
default=True,
help="Rename deliverable files to standard format (default: on)",
)
parser.add_argument(
"--setup-new",
action="store_true",
default=True,
help="Initialize folder structure for new empty projects (default: on)",
)
args = parser.parse_args()
if not CONSULTING_DIR.exists():
logger.error(f"Consulting directory not found: {CONSULTING_DIR}")
sys.exit(1)
logger.info(f"Consulting folder: {CONSULTING_DIR}")
logger.info(f"Dry run: {args.dry_run}\n")
archived, renamed, setup = [], [], []
if args.archive_completed:
logger.info("Archiving completed projects...")
archived = archive_completed_projects(
CONSULTING_DIR, ARCHIVE_DIR, logger, dry_run=args.dry_run
)
if args.rename_files:
logger.info("\nRenaming client deliverables...")
renamed = rename_client_files(CONSULTING_DIR, logger, dry_run=args.dry_run)
if args.setup_new:
logger.info("\nSetting up new project folders...")
setup = setup_new_project_folders(
CONSULTING_DIR, NEW_PROJECT_TEMPLATE, logger, dry_run=args.dry_run
)
print_summary(archived, renamed, setup, dry_run=args.dry_run)
if __name__ == "__main__":
main()
After the Script Runs
Maya runs the script in dry-run mode first:
Archiving completed projects...
[DRY RUN] Would archive: hartmann_logistics/ -> archived/hartmann_logistics/
Renaming client deliverables...
[DRY RUN] Would rename: [acme_corp] proposal_v1.docx -> 2024-01-15_acme_corp_proposal_v1.docx
[DRY RUN] Would rename: [acme_corp] proposal_v2.docx -> 2024-01-15_acme_corp_proposal_v2.docx
[DRY RUN] Would rename: [acme_corp] proposal_FINAL.docx -> 2024-01-15_acme_corp_proposal_final.docx
[DRY RUN] Would rename: [acme_corp] kickoff_notes.txt -> (skipped: .txt not a deliverable)
Setting up new project folders...
Setting up new project: belmont_realty/
[DRY RUN] Created: deliverables/
[DRY RUN] Created: drafts/
[DRY RUN] Created: client_materials/
[DRY RUN] Created: invoices/
[DRY RUN] Created: notes/
[DRY RUN] WEEKLY PROJECT ORGANIZER — SUMMARY
She notices the script would rename all three proposal versions (v1, v2, and FINAL) with today's date. That is not quite right — she only needs the current date prefix on active working files, not historical versions. She decides to move v1 and v2 to a drafts/ folder manually and only keep proposal_FINAL.docx in the root where the script will find it.
This is a common pattern in automation: the first dry run reveals an edge case you hadn't thought through. The fix here is not to change the script — it is to tidy up the existing mess first, then let the script maintain the standard going forward.
What Maya Learned
Convention is infrastructure. The status.txt file is a simple convention, not a database. It works because Maya is the only person who touches these folders. For a team environment, a shared spreadsheet or project management system would be the right signal source.
The script is only as good as your naming discipline. After running the organizer for a month, Maya noticed that the date-prefixed filenames made her file search results dramatically better — Spotlight searches for "acme_corp" now returned results in chronological order.
Automation reveals what matters. Building the script forced Maya to articulate what "organized" actually means for her project folder — something she had never written down. The NEW_PROJECT_TEMPLATE dictionary is now her onboarding document: when a new project starts, she knows exactly what folders to expect.