paths examplesGetting the path of the file you're currently writing code in - usually a pain with __file__ and Path.
from suitkaise .paths import Skpath
# Skpath() with no arguments returns the caller's file path
# this is the file where this line of code is written
current_file = Skpath( )
# get the absolute path (always uses forward slashes)
print(current_file.ap )
# "/Users/me/myproject/src/utils/helpers.py"
# get the path relative to project root
# (empty if the caller is outside the detected project root)
print(current_file.rp )
# "src/utils/helpers.py"
# get just the filename
print(current_file.name)
# "helpers.py"
from suitkaise .paths import Skpath
# Skpath().parent gives you the directory containing the current file
current_dir = Skpath( ).parent
# now you can access sibling files easily using truediv
config_file = current_dir / "config.json"
data_dir = current_dir / "data"
# check if they exist
if config_file.exists:
print(f"Config found at: {config_file.rp }")
from suitkaise import paths
# get_caller_path() is equivalent to Skpath()
current_file = paths .get_caller_path ()
# get_current_dir() is equivalent to Skpath().parent
current_dir = paths .get_current_dir ()
# get_cwd() returns the current working directory (where you ran python from)
cwd = paths .get_cwd ()
# these are different!
# - current_dir: directory containing THIS file
# - cwd: directory where `python script.py` was run from
from suitkaise import paths
from suitkaise .paths import Skpath
# get the project root (auto-detected)
root = paths .get_project_root ()
print(root .ap)
# "/Users/me/myproject"
# project root is detected by looking for:
# 1. setup.sk (suitkaise marker - highest priority)
# 2. setup.py, setup.cfg, pyproject.toml
# 3. .git, .gitignore
# 4. LICENSE, README files
# 5. requirements.txt
# you can also access root from any Skpath
some_file = Skpath( "src/main.py")
print(some_file.root .ap)
# "/Users/me/myproject"
from suitkaise import paths
from suitkaise .paths import CustomRoot
# set a custom root manually
paths .set_custom_root ("/my/custom/root")
# now all Skpath objects use this root
path = paths .get_project_root ()
print(path.ap )
# "/my/custom/root"
# clear it to go back to auto-detection
paths .clear_custom_root ()
# or use a context manager for temporary override
with CustomRoot( "/temp/project"):
# inside this block, root is /temp/project
root = paths .get_project_root ()
print(root .ap)
# "/temp/project"
# outside the block, root is auto-detected again
/from suitkaise .paths import Skpath
# Skpath supports the / operator like pathlib.Path
root = Skpath( ).root
data_file = root / "data" / "users.json"
# the result is always an Skpath
print(type(data_file))
# <class 'suitkaise.paths._int.skpath.Skpath'>
print(data_file.ap )
# "/Users/me/myproject/data/users.json"
print(data_file.rp )
# "data/users.json"
ap , rp , id , platform from suitkaise .paths import Skpath
path = Skpath( "src/utils/helpers.py")
# ap: absolute path (always forward slashes, even on Windows)
print(path.ap )
# "/Users/me/myproject/src/utils/helpers.py"
# rp: relative path to project root
print(path.rp )
# "src/utils/helpers.py"
# platform: absolute path with platform-native separators
print(path.platform )
# "C:\\Users\\me\\project\\src\\utils\\helpers.py" on Windows
# "/Users/me/project/src/utils/helpers.py" on Mac/Linux
# id: reversible base64url encoded ID
print(path.id )
# "c3JjL3V0aWxzL2hlbHBlcnMucHk"
# you can recreate the path from its ID!
same_path = Skpath( path.id )
print(same_path.rp )
# "src/utils/helpers.py"
pathlib compatibilityfrom suitkaise .paths import Skpath
path = Skpath( "src/main.py")
# all the familiar pathlib properties work
print(path.name) # "main.py"
print(path.stem) # "main"
print(path.suffix) # ".py"
print(path.parent ) # Skpath("src")
print(path.exists) # True or False
# pathlib methods work too
for py_file in path.parent .glob("*.py"):
print(py_file.name)
# recursive glob
for py_file in Skpath( ).root .rglob("*.py"):
print(py_file.rp )
from suitkaise .paths import Skpath
root = Skpath( ).root
# create directories
new_dir = root / "output/reports"
new_dir.mkdir(parents=True, exist_ok=True)
# create files
new_file = new_dir / "report.txt"
new_file.touch()
# copy files
source = root / "data/input.csv"
dest = source.copy_to(root / "backup/input.csv", parents=True)
# dest is an Skpath pointing to the copied file
# move files
temp_file = root / "temp/data.json"
final = temp_file.move_to(root / "data/final.json", overwrite=True)
# delete files
old_file = root / "temp/old.txt"
old_file.unlink(missing_ok=True)
# delete empty directories
empty_dir = root / "temp/empty"
empty_dir.rmdir()
from suitkaise .paths import Skpath
# Skpath always uses forward slashes internally
# this works on Windows, Mac, and Linux
path = Skpath( "data\\subdir\\file.txt") # Windows-style input
print(path.ap )
# "/Users/me/project/data/subdir/file.txt" (forward slashes)
# when you need OS-native separators (ex. for subprocess calls)
print(path.platform )
# Windows: "C:\\Users\\me\\project\\data\\subdir\\file.txt"
# Mac/Linux: "/Users/me/project/data/subdir/file.txt"
# works with open() and os functions automatically
with open(path, "r") as f: # uses __fspath__()
content = f.read()
from suitkaise .paths import is_valid_filename , streamline_path , streamline_path_quick
# check if a filename is valid on all platforms
print(is_valid_filename ("my_file.txt")) # True
print(is_valid_filename ("file<name>.txt")) # False (< and > are invalid)
print(is_valid_filename ("CON")) # False (Windows reserved name)
print(is_valid_filename ("")) # False (empty)
# sanitize a filename
clean = streamline_path ("My File<1>.txt", chars_to_replace=" ")
print(clean)
# "My_File_1_.txt"
# streamline_path_quick: simple version with common defaults
# - strips whitespace
# - replaces spaces
# - removes unicode
clean = streamline_path_quick ("My File наме.txt")
print(clean)
# "My_File_____.txt"
# limit length (preserves file extension)
short = streamline_path ("Very Long Filename That Needs Truncating.txt", max_len=10, chars_to_replace=" ")
print(short)
# "Very_Long_.txt"
from suitkaise import paths
# get all paths in the project (respects .gitignore)
all_paths = paths .get_project_paths ()
for p in all_paths[:5]:
print(p.rp )
# get as strings (more memory efficient for large projects)
all_paths_str = paths .get_project_paths (as_strings=True)
# exclude certain directories
paths_filtered = paths .get_project_paths (exclude=["node_modules", "dist", ".git"])
# get nested dictionary structure for something like a UI tree
structure = paths .get_project_structure ()
# {
# "myproject": {
# "src": {
# "main.py": {},
# "utils": {
# "helpers.py": {}
# }
# }
# }
# }
# get formatted tree string for something like a README
tree = paths .get_formatted_project_tree (depth=2)
print(tree)
# myproject/
# ├── src/
# │ ├── main.py
# │ └── utils/
# └── tests/
from suitkaise import paths
import json
# get the file path where a module is defined
json_path = paths .get_module_path (json)
print(json_path.ap )
# "/usr/lib/python3.11/json/__init__.py"
# works with classes too
from collections import OrderedDict
od_path = paths .get_module_path (OrderedDict)
print(od_path.ap )
# works with your own modules
from myapp.utils import MyClass
my_path = paths .get_module_path (MyClass)
print(my_path.rp )
# "myapp/utils.py"
import threading
from suitkaise .paths import Skpath , CustomRoot
from suitkaise import paths
# Skpath and root management functions are thread-safe
# multiple threads can safely:
# - create Skpath objects
# - access project root
# - use CustomRoot context manager
results = []
def worker(worker_id: int):
# each thread can safely create paths
path = Skpath( f"data/file_{worker_id}.txt")
# thread-safe access to properties
results.append({
"id": worker_id,
"ap": path.ap ,
"rp": path.rp ,
})
# start multiple threads
threads = []
for i in range(10):
t = threading.Thread(target=worker, args=(i,))
t.start()
threads.append(t)
for t in threads:
t.join()
print(f"Processed {len(results)} paths safely")
autopath decoratorThe decorator automatically converts path parameters based on type hints.
# WITHOUT autopath - you have to handle type conversion manually
def process_file_manual(path):
if isinstance(path, str):
path = Path(path)
elif isinstance(path, Skpath ):
path = Path(path.ap )
# ... now path is a Path
return path.read_text()
from suitkaise .paths import autopath , Skpath , AnyPath
from pathlib import Path
# WITH autopath - conversion is automatic based on type hints
@autopath ()
def process_file(path: Skpath ) -> str:
# path is guaranteed to be an Skpath
# you can pass str, Path, or Skpath - all get converted
return path.ap
# all of these work:
process_file("src/main.py") # str → Skpath
process_file(Path("src/main.py")) # Path → Skpath
process_file(Skpath( "src/main.py")) # Skpath → Skpath (no conversion needed)
autopath with different target typesfrom suitkaise .paths import autopath , Skpath , AnyPath
from pathlib import Path
@autopath ()
def needs_skpath(path: Skpath ) -> str:
# input is converted to Skpath
return path.rp
@autopath ()
def needs_path(path: Path) -> str:
# input is normalized through Skpath, then converted to Path
return str(path)
@autopath ()
def needs_string(path: str) -> str:
# input is normalized through Skpath, returns absolute path string
return path
@autopath ()
def needs_anypath(path: AnyPath ) -> str:
# AnyPath is a union of str | Path | Skpath
# autopath converts to Skpath (the richest type in the union)
return path.rp # path is Skpath
autopath with listsfrom suitkaise .paths import autopath , Skpath
from pathlib import Path
@autopath ()
def process_files(paths : list[Skpath ]) -> list[str]:
# each element in the list is converted to Skpath
return [p.rp for p in paths ]
# works with mixed input types
result = process_files([
"src/a.py", # str
Path("src/b.py"), # Path
Skpath( "src/c.py"), # Skpath
])
print(result)
# ["src/a.py", "src/b.py", "src/c.py"]
# also works with tuple, set, frozenset
@autopath ()
def process_set(paths : set[Skpath ]) -> int:
return len(paths )
autopath with use_caller optionfrom suitkaise .paths import autopath , Skpath
# use_caller=True fills in missing path arguments with caller's file
@autopath (use_caller=True)
def log_location(message: str, path: Skpath = None):
# if path is not provided, it becomes the file where log_location() was called
print(f"[{path.rp }] {message}")
# called without path argument
log_location("Starting process")
# prints: [src/main.py] Starting process
# called with explicit path
log_location("Found file", Skpath("data/input.csv"))
# prints: [data/input.csv] Found file
autopath with only optionfrom suitkaise .paths import autopath , Skpath
# only convert specific parameters (faster for large lists)
@autopath (only="file_path")
def process(file_path: str, tags: list[str], ids: list[str]):
# only file_path is normalized
# tags and ids are left unchanged (no conversion overhead)
return file_path
# useful when you have list[str] that aren't paths
result = process(
file_path="src/main.py",
tags=["python", "backend"],
ids=["abc123", "def456"]
)
Path IDs are perfect for database storage - they're URL-safe, reversible, and cross-platform.
import sqlite3
from suitkaise .paths import Skpath
from suitkaise import paths
# SETUP
conn = sqlite3.connect(":memory:")
conn.execute("""
CREATE TABLE files (
id TEXT PRIMARY KEY, -- path ID (base64url encoded)
name TEXT,
size INTEGER,
processed BOOLEAN
)
""")
# STORING PATHS
def store_file(path: Skpath , size: int):
"""Store a file record using its path ID."""
conn.execute(
"INSERT INTO files (id, name, size, processed) VALUES (?, ?, ?, ?)",
(path.id , path.name, size, False)
)
# store some files
for file_path in paths .get_project_paths ():
if file_path.is_file and file_path.suffix == ".py":
store_file(file_path, file_path.stat.st_size)
conn.commit()
# RETRIEVING PATHS
def get_unprocessed_files() -> list[Skpath ]:
"""Get all unprocessed files as Skpath objects."""
cursor = conn.execute(
"SELECT id FROM files WHERE processed = 0"
)
# reconstruct Skpath from stored ID
return [Skpath( row[0]) for row in cursor.fetchall()]
# get files and process them
for file_path in get_unprocessed_files():
print(f"Processing: {file_path.rp }")
# mark as processed
conn.execute(
"UPDATE files SET processed = 1 WHERE id = ?",
(file_path.id ,)
)
conn.commit()
import json
from pathlib import Path
from suitkaise .paths import Skpath
CACHE_DIR = Path(".cache")
CACHE_DIR.mkdir(exist_ok=True)
def get_cached_result(source_path: Skpath ) -> dict | None:
"""Get cached result for a file, or None if not cached."""
# use path.id as cache key - it's safe for filenames
cache_file = CACHE_DIR / f"{source_path.id }.json"
if cache_file.exists():
# check if cache is still valid (source hasn't changed)
cache_mtime = cache_file.stat().st_mtime
source_mtime = source_path.stat.st_mtime
if cache_mtime > source_mtime:
# cache is newer than source - use it
return json.loads(cache_file.read_text())
return None
def save_cached_result(source_path: Skpath , result: dict):
"""Save result to cache."""
cache_file = CACHE_DIR / f"{source_path.id }.json"
cache_file.write_text(json.dumps(result))
def process_with_cache(path: Skpath ) -> dict:
"""Process a file with caching."""
# try cache first
cached = get_cached_result(path)
if cached is not None:
print(f"Cache hit: {path.rp }")
return cached
# cache miss - do expensive processing
print(f"Processing: {path.rp }")
result = expensive_processing(path)
# save to cache
save_cached_result(path, result)
return result
from suitkaise .paths import Skpath
from suitkaise import paths
from dataclasses import dataclass
from datetime import datetime
@dataclass
class FileInfo:
path: Skpath
size: int
modified: datetime
def to_dict(self) -> dict:
return {
"id": self.path.id ,
"rp": self.path.rp ,
"name": self.path.name,
"size": self.size,
"modified": self.modified.isoformat(),
}
@classmethod
def from_path(cls, path: Skpath ) -> "FileInfo":
stat = path.stat
return cls(
path=path,
size=stat.st_size,
modified=datetime.fromtimestamp(stat.st_mtime),
)
def build_index(root: Skpath = None, extensions: list[str] = None) -> list[FileInfo]:
"""Build an index of files in the project."""
# use provided root or auto-detect
if root is None:
root = paths .get_project_root ()
index = []
# get all project paths (respects .gitignore)
for file_path in paths .get_project_paths (root=root):
# skip directories
if not file_path.is_file:
continue
# filter by extension if specified
if extensions and file_path.suffix not in extensions:
continue
# add to index
index.append(FileInfo.from_path(file_path))
return index
# build index of Python files
py_index = build_index(extensions=[".py"])
for info in py_index[:5]:
print(f"{info.path.rp }: {info.size} bytes")
import json
from suitkaise .paths import Skpath , autopath , AnyPath
class ConfigLoader:
"""Load config files with paths relative to config location."""
def __init__(self, config_path: AnyPath ):
# convert to Skpath for easy path manipulation
self.config_path = Skpath( config_path)
self.config_dir = self.config_path.parent
self.config = self._load()
def _load(self) -> dict:
"""Load the config file."""
with open(self.config_path, "r") as f:
return json.load(f)
def resolve_path(self, relative_path: str) -> Skpath :
"""Resolve a path relative to the config file location."""
return self.config_dir / relative_path
def get_data_dir(self) -> Skpath :
"""Get the data directory from config."""
# config might specify: "data_dir": "./data"
# we need to resolve it relative to config location
data_dir = self.config.get("data_dir", "data")
return self.resolve_path(data_dir)
def get_input_files(self) -> list[Skpath ]:
"""Get input file paths from config."""
# config might specify: "input_files": ["input1.csv", "input2.csv"]
input_files = self.config.get("input_files", [])
data_dir = self.get_data_dir()
return [data_dir / f for f in input_files]
# usage
config = ConfigLoader("configs/production.json")
print(f"Data dir: {config.get_data_dir().rp }")
for input_file in config.get_input_files():
print(f"Input: {input_file.rp }")
paths A complete file organizer that demonstrates cross-platform compatibility, advanced usage, , and .
"""
File Organizer
Organizes files by type into categorized directories.
Demonstrates:
- Cross-platform path handling
- autopath decorator
- AnyPath type hints
- Project root detection
- Path ID encoding
- File operations
"""
import json
import shutil
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from suitkaise import paths
from suitkaise .paths import (
Skpath ,
AnyPath ,
autopath ,
PathDetectionError ,
streamline_path_quick ,
)
# CONFIGURATION
# file type categories
FILE_CATEGORIES = {
"images": [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico"],
"documents": [".pdf", ".doc", ".docx", ".txt", ".md", ".rst"],
"code": [".py", ".js", ".ts", ".html", ".css", ".json", ".yaml", ".yml"],
"data": [".csv", ".xlsx", ".xls", ".sql", ".db"],
"archives": [".zip", ".tar", ".gz", ".rar", ".7z"],
}
# DATA CLASSES
@dataclass
class OrganizedFile:
"""Record of a file that was organized."""
original_path: str # original relative path
new_path: str # new relative path
category: str # file category
size: int # file size in bytes
organized_at: str # ISO timestamp
path_id: str # reversible path ID
@dataclass
class OrganizeResult:
"""Result of an organize operation."""
source_dir: str
output_dir: str
files_organized: int = 0
files_skipped: int = 0
errors: list[str] = field(default_factory=list)
organized_files: list[OrganizedFile] = field(default_factory=list)
# HELPER FUNCTIONS
@autopath ()
def get_category(file_path: Skpath ) -> str | None:
"""
Get the category for a file based on its extension.
Returns None if file doesn't match any category.
"""
suffix = file_path.suffix.lower()
for category, extensions in FILE_CATEGORIES.items():
if suffix in extensions:
return category
return None
@autopath ()
def ensure_directory(path: Skpath ) -> Skpath :
"""
Ensure a directory exists, creating it if necessary.
Uses autopath so you can pass str, Path, or Skpath .
"""
if not path.exists:
path.mkdir(parents=True, exist_ok=True)
return path
@autopath ()
def safe_copy(source: Skpath , dest: Skpath , overwrite: bool = False) -> Skpath | None:
"""
Safely copy a file, handling name conflicts.
Returns the destination path, or None if skipped.
"""
# check if destination already exists
if dest.exists and not overwrite:
# generate unique name
stem = dest.stem
suffix = dest.suffix
parent = dest.parent
counter = 1
while dest.exists:
new_name = f"{stem}_{counter}{suffix}"
dest = parent / new_name
counter += 1
# ensure parent directory exists
ensure_directory(dest.parent)
# copy the file
return source.copy_to(dest, overwrite=overwrite)
# MAIN ORGANIZER CLASS
class FileOrganizer:
"""
Organizes files from a source directory into categorized folders.
Cross-platform compatible:
- Uses Skpath for normalized path handling
- Works on Windows, Mac, and Linux
- Stores path IDs for database compatibility
"""
def __init__(self, output_dir: AnyPath = None):
"""
Initialize the organizer.
Args:
output_dir: Where to organize files to.
If None, uses "organized" in project root.
"""
# get project root for default paths
try:
self.project_root = paths.get_project_root ()
except PathDetectionError :
# no project root found - use current directory
self.project_root = paths.get_cwd ()
# set output directory
if output_dir is None:
self.output_dir = self.project_root / "organized"
else:
self.output_dir = Skpath( output_dir)
# ensure output directory exists
ensure_directory(self.output_dir )
# manifest file to track organized files
self.manifest_path = self.output_dir / "manifest.json"
self.manifest: list[OrganizedFile] = []
# load existing manifest if present
self._load_manifest()
def _load_manifest(self):
"""Load the manifest file if it exists."""
if self.manifest_path .exists:
with open(self.manifest_path , "r") as f:
data = json.load(f)
self.manifest = [
OrganizedFile(**item) for item in data
]
def _save_manifest(self):
"""Save the manifest file."""
with open(self.manifest_path, "w") as f:
json.dump(
[vars(item) for item in self.manifest],
f,
indent=2
)
@autopath ()
def organize(
self,
source_dir: Skpath ,
recursive: bool = True,
dry_run: bool = False
) -> OrganizeResult:
"""
Organize files from source directory into categories.
Args:
source_dir: Directory to organize files from.
Accepts str, Path, or Skpath (via autopath ).
recursive: Whether to process subdirectories.
dry_run: If True, don't actually move files.
Returns:
OrganizeResult with summary of operations.
"""
result = OrganizeResult(
source_dir=source_dir.rp ,
output_dir=self.output_dir.rp ,
)
# get files to organize
if recursive:
files = list(source_dir.rglob("*"))
else:
files = list(source_dir.iterdir())
for file_path in files:
# skip directories
if not file_path.is_file:
continue
# skip hidden files
if file_path.name.startswith("."):
result.files_skipped += 1
continue
# get category
category = get_category(file_path)
if category is None:
result.files_skipped += 1
continue
# build destination path
# sanitize filename for cross-platform compatibility
safe_name = streamline_path_quick (file_path.name)
dest_path = self.output_dir / category / safe_name
if dry_run:
# just record what would happen
print(f"Would organize: {file_path.rp } → {dest_path.rp }")
result.files_organized += 1
continue
# actually copy the file
try:
copied = safe_copy(file_path, dest_path)
if copied:
# record the operation
record = OrganizedFile(
original_path=file_path.rp ,
new_path=copied.rp ,
category=category,
size=file_path.stat.st_size,
organized_at=datetime.now().isoformat(),
path_id=copied.id , # store path ID for database
)
self.manifest.append(record)
result.organized_files.append(record)
result.files_organized += 1
else:
result.files_skipped += 1
except Exception as e:
result.errors.append(f"{file_path.rp }: {str(e)}")
# save manifest
if not dry_run:
self._save_manifest()
return result
def get_file_by_id(self, path_id: str) -> Skpath | None:
"""
Get an organized file by its path ID.
Path IDs are reversible - you can reconstruct the full path.
This is useful for database storage.
"""
# find in manifest
for record in self.manifest:
if record.path_id == path_id:
return Skpath( path_id)
return None
def get_files_by_category(self, category: str) -> list[Skpath ]:
"""Get all organized files in a category."""
return [
Skpath( record.path_id)
for record in self.manifest
if record.category == category
]
def print_summary(self):
"""Print a summary of organized files."""
print(f"\n{'='*50}")
print("File Organizer Summary")
print(f"{'='*50}")
print(f"Output directory: {self.output_dir.rp }")
print(f"Total files: {len(self.manifest)}")
print()
# count by category
by_category = {}
for record in self.manifest:
by_category[record.category] = by_category.get(record.category, 0) + 1
print("By category:")
for category, count in sorted(by_category.items()):
print(f" {category}: {count} files")
# show directory tree
print()
print("Directory structure:")
tree = paths .get_formatted_project_tree (
root=self.output_dir,
depth=2,
include_files=False
)
print(tree)
# MAIN
def main():
"""Main entry point (self-contained)."""
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
tmp_root = Skpath( tmpdir)
source_dir = tmp_root / "downloads"
output_dir = tmp_root / "organized"
# seed some sample files
(source_dir / "notes.txt").write_text("notes\n" * 10)
(source_dir / "script.py").write_text("print('hello')\n")
(source_dir / "data.json").write_text('{"ok": true}\n')
(source_dir / "image.png").write_bytes(b"\x89PNG\r\n\x1a\n")
# create organizer with temp output directory
organizer = FileOrganizer(output_dir=output_dir)
print(f"Organizing files from: {source_dir.rp }")
print(f"Output directory: {organizer.output_dir.rp }")
print()
# dry run
print("Dry run:")
result = organizer.organize(source_dir, dry_run=True)
print(f"Would organize {result.files_organized} files")
print(f"Would skip {result.files_skipped} files")
print()
# actually organize
print("Organizing...")
result = organizer.organize(source_dir, dry_run=False)
# print results
print(f"Organized: {result.files_organized} files")
print(f"Skipped: {result.files_skipped} files")
if result.errors:
print(f"Errors: {len(result.errors)}")
for error in result.errors[:5]:
print(f" - {error}")
# show summary
organizer.print_summary()
# demonstrate path ID retrieval
if organizer.manifest:
print()
print("Path ID demonstration:")
record = organizer.manifest[0]
print(f" Original: {record.original_path}")
print(f" Path ID: {record.path_id}")
# reconstruct path from ID
reconstructed = Skpath( record.path_id)
print(f" Reconstructed: {reconstructed.rp }")
if __name__ == "__main__":
main()