generated from nhcarrigan/template
feat: add multilingual support so Naomi can use Python too
This commit is contained in:
+64
-30
@@ -1,47 +1,81 @@
|
|||||||
name: Node.js CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [main]
|
||||||
- main
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches: [main]
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
dependency-pin-check-typescript:
|
||||||
name: CI
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Source Files
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
- name: Copy TypeScript files to root
|
||||||
|
run: |
|
||||||
|
cp typescript/package.json .
|
||||||
|
cp typescript/package-lock.json . 2>/dev/null || true
|
||||||
|
cp typescript/pnpm-lock.yaml . 2>/dev/null || true
|
||||||
|
- uses: naomi-lgbt/dependency-pin-check@main
|
||||||
|
|
||||||
- name: Use Node.js v24
|
dependency-pin-check-python:
|
||||||
uses: actions/setup-node@v4
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Copy Python files to root
|
||||||
|
run: |
|
||||||
|
cp python/requirements.txt .
|
||||||
|
cp python/pyproject.toml .
|
||||||
|
- uses: naomi-lgbt/dependency-pin-check@main
|
||||||
|
|
||||||
|
typescript:
|
||||||
|
needs: dependency-pin-check-typescript
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10.15.0
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: typescript/pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Install dependencies
|
||||||
uses: pnpm/action-setup@v2
|
run: make install-ts
|
||||||
|
|
||||||
|
- name: Run ESLint
|
||||||
|
run: make lint-ts
|
||||||
|
|
||||||
|
- name: Build TypeScript
|
||||||
|
run: make build
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: make test
|
||||||
|
|
||||||
|
python:
|
||||||
|
needs: dependency-pin-check-python
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
version: 10
|
python-version: '3.12'
|
||||||
|
|
||||||
- name: Ensure Dependencies are Pinned
|
- name: Install uv
|
||||||
uses: naomi-lgbt/dependency-pin-check@main
|
uses: astral-sh/setup-uv@v5
|
||||||
with:
|
with:
|
||||||
language: javascript
|
enable-cache: true
|
||||||
dev-dependencies: true
|
|
||||||
peer-dependencies: true
|
|
||||||
optional-dependencies: true
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: make install-py
|
||||||
|
|
||||||
- name: Lint Source Files
|
- name: Run Ruff linter
|
||||||
run: pnpm run lint
|
run: make lint-py
|
||||||
|
|
||||||
- name: Verify Build
|
- name: Check Ruff formatting
|
||||||
run: pnpm run build
|
run: make format-check-py
|
||||||
|
|
||||||
- name: Run Tests
|
|
||||||
run: pnpm run test
|
|
||||||
+15
-2
@@ -1,4 +1,17 @@
|
|||||||
node_modules
|
# Project-specific
|
||||||
prod
|
prod
|
||||||
data
|
data
|
||||||
!data/.gitkeep
|
!data/.gitkeep
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
prod.env
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
.PHONY: help install install-ts install-py build lint lint-ts lint-py format format-py format-check format-check-py test clean run
|
||||||
|
|
||||||
|
# Default target - show help
|
||||||
|
help:
|
||||||
|
@echo "Available commands:"
|
||||||
|
@echo " make install - Install all dependencies (TypeScript and Python)"
|
||||||
|
@echo " make install-ts - Install TypeScript dependencies only"
|
||||||
|
@echo " make install-py - Install Python dependencies only"
|
||||||
|
@echo " make build - Build TypeScript (type check)"
|
||||||
|
@echo " make lint - Run all linters (TypeScript and Python)"
|
||||||
|
@echo " make lint-ts - Run TypeScript linter only"
|
||||||
|
@echo " make lint-py - Run Python linter only"
|
||||||
|
@echo " make format - Format Python code"
|
||||||
|
@echo " make format-check - Check Python formatting without modifying"
|
||||||
|
@echo " make test - Run tests"
|
||||||
|
@echo " make clean - Clean build artifacts and caches"
|
||||||
|
@echo ""
|
||||||
|
@echo "Running scripts:"
|
||||||
|
@echo " make run - Interactive script runner (select language, category, script)"
|
||||||
|
|
||||||
|
# Install all dependencies
|
||||||
|
install: install-ts install-py
|
||||||
|
|
||||||
|
# TypeScript dependencies
|
||||||
|
install-ts:
|
||||||
|
cd typescript && pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# Python dependencies
|
||||||
|
install-py:
|
||||||
|
cd python && uv venv
|
||||||
|
cd python && uv pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Build TypeScript
|
||||||
|
build:
|
||||||
|
cd typescript && pnpm exec tsc --noEmit
|
||||||
|
|
||||||
|
# Run all linters
|
||||||
|
lint: lint-ts lint-py
|
||||||
|
|
||||||
|
# TypeScript linting
|
||||||
|
lint-ts:
|
||||||
|
cd typescript && pnpm exec eslint src --max-warnings 0
|
||||||
|
|
||||||
|
# Python linting
|
||||||
|
lint-py:
|
||||||
|
cd python && uv run ruff check .
|
||||||
|
|
||||||
|
# Format Python code
|
||||||
|
format: format-py
|
||||||
|
|
||||||
|
format-py:
|
||||||
|
cd python && uv run ruff format .
|
||||||
|
|
||||||
|
# Check formatting without modifying
|
||||||
|
format-check: format-check-py
|
||||||
|
|
||||||
|
format-check-py:
|
||||||
|
cd python && uv run ruff format --check .
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
test:
|
||||||
|
@echo "No tests configured yet"
|
||||||
|
@exit 0
|
||||||
|
|
||||||
|
# Clean build artifacts and caches
|
||||||
|
clean:
|
||||||
|
rm -rf typescript/node_modules
|
||||||
|
rm -rf python/.venv
|
||||||
|
rm -rf python/.ruff_cache
|
||||||
|
find python -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find python -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||||
|
|
||||||
|
# Interactive script runner
|
||||||
|
run:
|
||||||
|
@./run.sh
|
||||||
@@ -1,20 +1,132 @@
|
|||||||
# New Repository Template
|
# Ephemere
|
||||||
|
|
||||||
This template contains all of our basic files for a new GitHub repository. There is also a handy workflow that will create an issue on a new repository made from this template, with a checklist for the steps we usually take in setting up a new repository.
|
A collection of ephemeral scripts for various tasks, written in TypeScript and Python.
|
||||||
|
|
||||||
If you're starting a Node.JS project with TypeScript, we have a [specific template](https://github.com/naomi-lgbt/nodejs-typescript-template) for that purpose.
|
## Project Structure
|
||||||
|
|
||||||
## Readme
|
```
|
||||||
|
.
|
||||||
|
├── typescript/ # TypeScript project
|
||||||
|
│ ├── src/ # TypeScript source files
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── tsconfig.json
|
||||||
|
│ └── eslint.config.js
|
||||||
|
├── python/ # Python project
|
||||||
|
│ ├── *.py # Python scripts
|
||||||
|
│ ├── pyproject.toml
|
||||||
|
│ └── requirements.txt
|
||||||
|
├── Makefile # Build commands for both projects
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
Delete all of the above text (including this line), and uncomment the below text to use our standard readme template.
|
## Setup
|
||||||
|
|
||||||
<!-- # Project Name
|
### Prerequisites
|
||||||
|
|
||||||
Project Description
|
- Node.js (v24+) with nvm
|
||||||
|
- Python 3.10+
|
||||||
|
- pnpm 10.15.0
|
||||||
|
- uv (Python package manager)
|
||||||
|
- 1Password CLI (for secrets management)
|
||||||
|
|
||||||
## Live Version
|
### Installation
|
||||||
|
|
||||||
This page is currently deployed. [View the live website.]
|
Install all dependencies (TypeScript and Python):
|
||||||
|
```bash
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
Or install individually:
|
||||||
|
```bash
|
||||||
|
make install-ts # TypeScript dependencies only
|
||||||
|
make install-py # Python dependencies only
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### TypeScript Scripts
|
||||||
|
|
||||||
|
TypeScript scripts are located in the `typescript/src/` directory. To run a TypeScript script with environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the root directory
|
||||||
|
make run-ts src/s3/upload.ts
|
||||||
|
|
||||||
|
# Or manually from typescript directory
|
||||||
|
cd typescript
|
||||||
|
pnpm start path/to/script.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python Scripts
|
||||||
|
|
||||||
|
Python scripts are located in the `python/` directory. To run a Python script with environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the root directory
|
||||||
|
make run-py analyse_availability.py
|
||||||
|
|
||||||
|
# Or manually from python directory
|
||||||
|
cd python
|
||||||
|
uv run python script_name.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Linting and Formatting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all linters (TypeScript and Python)
|
||||||
|
make lint
|
||||||
|
|
||||||
|
# Run linters individually
|
||||||
|
make lint-ts # TypeScript linter
|
||||||
|
make lint-py # Python linter
|
||||||
|
|
||||||
|
# Build TypeScript (type check)
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Format Python code
|
||||||
|
make format
|
||||||
|
|
||||||
|
# Check Python formatting without modifying
|
||||||
|
make format-check
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Clean build artifacts and caches
|
||||||
|
make clean
|
||||||
|
|
||||||
|
# Show all available commands
|
||||||
|
make help
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
The GitHub Actions workflow runs the following checks:
|
||||||
|
|
||||||
|
1. **Dependency pin check** - Ensures all dependencies are pinned to exact versions
|
||||||
|
2. **TypeScript checks**:
|
||||||
|
- ESLint
|
||||||
|
- TypeScript build (type checking)
|
||||||
|
- Tests
|
||||||
|
3. **Python checks**:
|
||||||
|
- Ruff linting
|
||||||
|
- Ruff format checking
|
||||||
|
|
||||||
|
## Secrets Management
|
||||||
|
|
||||||
|
This project uses 1Password CLI for secrets management. Environment variables are stored in `prod.env` as 1Password vault references.
|
||||||
|
|
||||||
|
The `make run-ts` and `make run-py` commands automatically inject secrets from 1Password:
|
||||||
|
```bash
|
||||||
|
# These commands include 1Password integration
|
||||||
|
make run-ts src/discord/bot.ts
|
||||||
|
make run-py evaluate_technical_proficiency.py
|
||||||
|
```
|
||||||
|
|
||||||
|
To manually run scripts with secrets:
|
||||||
|
```bash
|
||||||
|
op run --env-file=prod.env -- <command>
|
||||||
|
```
|
||||||
|
|
||||||
## Feedback and Bugs
|
## Feedback and Bugs
|
||||||
|
|
||||||
@@ -36,4 +148,4 @@ Copyright held by Naomi Carrigan.
|
|||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`. -->
|
We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
|
||||||
|
# Virtual Environment
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# uv
|
||||||
|
.uv/
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
.pytest_cache/
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
.ruff_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.pylintrc
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
.PHONY: help install lint format format-check clean
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Python project commands:"
|
||||||
|
@echo " make install - Set up virtual environment and install dependencies"
|
||||||
|
@echo " make lint - Run Ruff linter"
|
||||||
|
@echo " make format - Format code with Ruff"
|
||||||
|
@echo " make format-check - Check formatting without modifying"
|
||||||
|
@echo " make clean - Clean Python artifacts"
|
||||||
|
|
||||||
|
install:
|
||||||
|
uv venv
|
||||||
|
uv pip install -r requirements.txt
|
||||||
|
|
||||||
|
lint:
|
||||||
|
uv run ruff check .
|
||||||
|
|
||||||
|
format:
|
||||||
|
uv run ruff format .
|
||||||
|
|
||||||
|
format-check:
|
||||||
|
uv run ruff format --check .
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf .venv
|
||||||
|
rm -rf .ruff_cache
|
||||||
|
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
||||||
|
|
||||||
|
UTC_BLOCKS = {
|
||||||
|
'mornings': (6, 12), # 06:00 - 12:00 UTC
|
||||||
|
'afternoons': (12, 18), # 12:00 - 18:00 UTC
|
||||||
|
'evenings': (18, 24), # 18:00 - 00:00 UTC
|
||||||
|
'nights': (0, 6) # 00:00 - 06:00 UTC
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse_utc_offset(timezone_str: str) -> float:
|
||||||
|
"""Extract UTC offset from timezone string like 'America/New_York (UTC-5)'"""
|
||||||
|
match = re.search(r'UTC([+-]?\d+(?::\d+)?)', timezone_str)
|
||||||
|
if match:
|
||||||
|
offset_str = match.group(1)
|
||||||
|
if ':' in offset_str:
|
||||||
|
parts = offset_str.split(':')
|
||||||
|
hours = int(parts[0])
|
||||||
|
minutes = int(parts[1]) if len(parts) > 1 else 0
|
||||||
|
if hours < 0:
|
||||||
|
return hours - minutes / 60
|
||||||
|
return hours + minutes / 60
|
||||||
|
return float(offset_str)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def parse_time_slots(time_str: str) -> list[tuple[int, int]]:
|
||||||
|
"""Parse time slots like '17:00-18:00' or '07:00-08:00; 19:00-20:00'"""
|
||||||
|
slots = []
|
||||||
|
if not time_str or time_str.lower() in ['n/a', 'na', '']:
|
||||||
|
return slots
|
||||||
|
|
||||||
|
parts = time_str.split(';')
|
||||||
|
for part in parts:
|
||||||
|
part = part.strip()
|
||||||
|
match = re.search(r'(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})', part)
|
||||||
|
if match:
|
||||||
|
start_hour = int(match.group(1))
|
||||||
|
end_hour = int(match.group(3))
|
||||||
|
slots.append((start_hour, end_hour))
|
||||||
|
return slots
|
||||||
|
|
||||||
|
def local_hour_to_utc(local_hour: int, utc_offset: float) -> int:
|
||||||
|
"""Convert local hour to UTC hour"""
|
||||||
|
utc_hour = local_hour - utc_offset
|
||||||
|
return int(utc_hour) % 24
|
||||||
|
|
||||||
|
def get_utc_blocks_for_hour(utc_hour: int) -> list[str]:
|
||||||
|
"""Determine which UTC block(s) an hour falls into"""
|
||||||
|
blocks = []
|
||||||
|
for block_name, (start, end) in UTC_BLOCKS.items():
|
||||||
|
if block_name == 'nights':
|
||||||
|
if utc_hour >= 0 and utc_hour < 6:
|
||||||
|
blocks.append(block_name)
|
||||||
|
elif block_name == 'evenings':
|
||||||
|
if utc_hour >= 18 and utc_hour < 24:
|
||||||
|
blocks.append(block_name)
|
||||||
|
else:
|
||||||
|
if utc_hour >= start and utc_hour < end:
|
||||||
|
blocks.append(block_name)
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
def analyze_applicant_availability(timezone_str: str, day_slots: dict) -> dict:
|
||||||
|
"""Analyze availability for one applicant"""
|
||||||
|
utc_offset = parse_utc_offset(timezone_str)
|
||||||
|
|
||||||
|
block_counts = defaultdict(int)
|
||||||
|
all_utc_hours = set()
|
||||||
|
|
||||||
|
for day in DAYS:
|
||||||
|
slots = day_slots.get(day, [])
|
||||||
|
for start_hour, end_hour in slots:
|
||||||
|
for hour in range(start_hour, end_hour):
|
||||||
|
utc_hour = local_hour_to_utc(hour, utc_offset)
|
||||||
|
all_utc_hours.add(utc_hour)
|
||||||
|
blocks = get_utc_blocks_for_hour(utc_hour)
|
||||||
|
for block in blocks:
|
||||||
|
block_counts[block] += 1
|
||||||
|
|
||||||
|
available_blocks = []
|
||||||
|
for block in ['mornings', 'afternoons', 'evenings', 'nights']:
|
||||||
|
if block_counts[block] >= 3:
|
||||||
|
available_blocks.append(block)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'utc_offset': utc_offset,
|
||||||
|
'timezone': timezone_str,
|
||||||
|
'available_blocks': available_blocks,
|
||||||
|
'block_counts': dict(block_counts),
|
||||||
|
'total_unique_utc_hours': len(all_utc_hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse_table_md() -> list[dict]:
|
||||||
|
"""Parse table.md and extract availability data"""
|
||||||
|
with open('table.md', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
lines = content.strip().split('\n')
|
||||||
|
|
||||||
|
header_idx = None
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.startswith('| Discord ID'):
|
||||||
|
header_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if header_idx is None:
|
||||||
|
raise ValueError("Could not find table header")
|
||||||
|
|
||||||
|
headers = [h.strip() for h in lines[header_idx].split('|')[1:-1]]
|
||||||
|
|
||||||
|
applicants = []
|
||||||
|
for line in lines[header_idx + 2:]:
|
||||||
|
if not line.startswith('|'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
cells = [c.strip() for c in line.split('|')[1:-1]]
|
||||||
|
if len(cells) < len(headers):
|
||||||
|
continue
|
||||||
|
|
||||||
|
row = dict(zip(headers, cells))
|
||||||
|
applicants.append(row)
|
||||||
|
|
||||||
|
return applicants
|
||||||
|
|
||||||
|
def main():
|
||||||
|
with open('discord_verification.json', 'r') as f:
|
||||||
|
verification = json.load(f)
|
||||||
|
|
||||||
|
verified_ids = set(v[0] for v in verification['verified'])
|
||||||
|
print(f"Verified applicants: {len(verified_ids)}")
|
||||||
|
|
||||||
|
applicants = parse_table_md()
|
||||||
|
print(f"Total applicants in table: {len(applicants)}")
|
||||||
|
|
||||||
|
availability_results = []
|
||||||
|
|
||||||
|
for applicant in applicants:
|
||||||
|
discord_id = applicant.get('Discord ID', '')
|
||||||
|
if discord_id not in verified_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
timezone = applicant.get('Timezone', '')
|
||||||
|
|
||||||
|
day_slots = {}
|
||||||
|
for day in DAYS:
|
||||||
|
time_str = applicant.get(day, '')
|
||||||
|
day_slots[day] = parse_time_slots(time_str)
|
||||||
|
|
||||||
|
analysis = analyze_applicant_availability(timezone, day_slots)
|
||||||
|
|
||||||
|
availability_results.append({
|
||||||
|
'discord_id': discord_id,
|
||||||
|
'timezone': timezone,
|
||||||
|
'utc_offset': analysis['utc_offset'],
|
||||||
|
'available_blocks': analysis['available_blocks'],
|
||||||
|
'block_counts': analysis['block_counts'],
|
||||||
|
'total_unique_utc_hours': analysis['total_unique_utc_hours']
|
||||||
|
})
|
||||||
|
|
||||||
|
with open('availability_analysis.json', 'w') as f:
|
||||||
|
json.dump(availability_results, f, indent=2)
|
||||||
|
|
||||||
|
block_distribution = defaultdict(int)
|
||||||
|
for result in availability_results:
|
||||||
|
for block in result['available_blocks']:
|
||||||
|
block_distribution[block] += 1
|
||||||
|
|
||||||
|
print(f"\n=== AVAILABILITY ANALYSIS COMPLETE ===")
|
||||||
|
print(f"Analyzed: {len(availability_results)} applicants")
|
||||||
|
print(f"\nBlock Distribution (applicants available in each block):")
|
||||||
|
for block in ['mornings', 'afternoons', 'evenings', 'nights']:
|
||||||
|
print(f" {block.capitalize()}: {block_distribution[block]}")
|
||||||
|
|
||||||
|
no_blocks = sum(1 for r in availability_results if not r['available_blocks'])
|
||||||
|
print(f"\nApplicants with no clear block availability: {no_blocks}")
|
||||||
|
|
||||||
|
print(f"\nResults saved to availability_analysis.json")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# GitHub API (no auth needed for public repos, but rate limited)
|
||||||
|
GITHUB_API = "https://api.github.com"
|
||||||
|
|
||||||
|
def extract_github_info(url: str) -> tuple[Optional[str], Optional[str]]:
|
||||||
|
"""Extract owner and repo from GitHub URL."""
|
||||||
|
# Handle various GitHub URL formats
|
||||||
|
patterns = [
|
||||||
|
r'github\.com/([^/]+)/([^/\s?#]+)', # github.com/owner/repo
|
||||||
|
r'github\.com/([^/\s?#]+)/?$', # github.com/owner (profile)
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
match = re.search(pattern, url)
|
||||||
|
if match:
|
||||||
|
groups = match.groups()
|
||||||
|
if len(groups) == 2:
|
||||||
|
return groups[0], groups[1].rstrip('.git')
|
||||||
|
elif len(groups) == 1:
|
||||||
|
return groups[0], None
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def fetch_github_user(username: str) -> Optional[dict]:
|
||||||
|
"""Fetch GitHub user profile."""
|
||||||
|
url = f"{GITHUB_API}/users/{username}"
|
||||||
|
req = urllib.request.Request(url)
|
||||||
|
req.add_header("Accept", "application/vnd.github.v3+json")
|
||||||
|
req.add_header("User-Agent", "Cohort-Evaluator")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = urllib.request.urlopen(req, timeout=10)
|
||||||
|
return json.loads(response.read().decode())
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def fetch_github_repos(username: str) -> list:
|
||||||
|
"""Fetch user's public repos."""
|
||||||
|
url = f"{GITHUB_API}/users/{username}/repos?per_page=100&sort=updated"
|
||||||
|
req = urllib.request.Request(url)
|
||||||
|
req.add_header("Accept", "application/vnd.github.v3+json")
|
||||||
|
req.add_header("User-Agent", "Cohort-Evaluator")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = urllib.request.urlopen(req, timeout=10)
|
||||||
|
return json.loads(response.read().decode())
|
||||||
|
except Exception as e:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def fetch_repo_languages(owner: str, repo: str) -> dict:
|
||||||
|
"""Fetch languages used in a repo."""
|
||||||
|
url = f"{GITHUB_API}/repos/{owner}/{repo}/languages"
|
||||||
|
req = urllib.request.Request(url)
|
||||||
|
req.add_header("Accept", "application/vnd.github.v3+json")
|
||||||
|
req.add_header("User-Agent", "Cohort-Evaluator")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = urllib.request.urlopen(req, timeout=10)
|
||||||
|
return json.loads(response.read().decode())
|
||||||
|
except Exception as e:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def analyze_proficiency_text(text: str) -> tuple[str, list[str]]:
|
||||||
|
"""Analyze self-described proficiency text."""
|
||||||
|
text_lower = text.lower()
|
||||||
|
|
||||||
|
# Extract languages/technologies mentioned
|
||||||
|
tech_patterns = [
|
||||||
|
r'\b(python|java|javascript|typescript|c\+\+|c#|ruby|go|rust|swift|kotlin|php|perl|scala|r)\b',
|
||||||
|
r'\b(react|angular|vue|node|express|django|flask|spring|rails|laravel)\b',
|
||||||
|
r'\b(html|css|sass|scss|tailwind|bootstrap)\b',
|
||||||
|
r'\b(sql|mysql|postgresql|mongodb|redis|firebase)\b',
|
||||||
|
r'\b(docker|kubernetes|aws|azure|gcp|git)\b',
|
||||||
|
r'\b(machine learning|ml|ai|data science|tensorflow|pytorch)\b',
|
||||||
|
]
|
||||||
|
|
||||||
|
technologies = set()
|
||||||
|
for pattern in tech_patterns:
|
||||||
|
matches = re.findall(pattern, text_lower)
|
||||||
|
technologies.update(matches)
|
||||||
|
|
||||||
|
# Determine level from keywords
|
||||||
|
beginner_keywords = ['beginner', 'learning', 'new to', 'just started', 'basic', 'novice', 'early']
|
||||||
|
intermediate_keywords = ['intermediate', 'comfortable', 'familiar', 'some experience', 'worked with']
|
||||||
|
advanced_keywords = ['advanced', 'expert', 'senior', 'professional', 'years of experience', 'proficient', 'strong']
|
||||||
|
|
||||||
|
level = 'intermediate' # default
|
||||||
|
|
||||||
|
if any(kw in text_lower for kw in advanced_keywords):
|
||||||
|
level = 'advanced'
|
||||||
|
elif any(kw in text_lower for kw in beginner_keywords):
|
||||||
|
level = 'beginner'
|
||||||
|
elif any(kw in text_lower for kw in intermediate_keywords):
|
||||||
|
level = 'intermediate'
|
||||||
|
|
||||||
|
return level, list(technologies)
|
||||||
|
|
||||||
|
def evaluate_applicant(applicant: dict, index: int, total: int) -> dict:
|
||||||
|
"""Evaluate a single applicant's technical proficiency."""
|
||||||
|
discord_id = applicant['discord_id']
|
||||||
|
project_url = applicant['project_url']
|
||||||
|
proficiency_self = applicant['proficiency_self']
|
||||||
|
project_reason = applicant['project_reason']
|
||||||
|
|
||||||
|
print(f"[{index+1}/{total}] Evaluating {discord_id}...")
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'discord_id': discord_id,
|
||||||
|
'github_username': None,
|
||||||
|
'github_repos_count': 0,
|
||||||
|
'github_followers': 0,
|
||||||
|
'languages_from_github': [],
|
||||||
|
'languages_from_text': [],
|
||||||
|
'self_described_level': None,
|
||||||
|
'final_proficiency': 'intermediate', # default
|
||||||
|
'tech_stack': [],
|
||||||
|
'notes': []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Analyze self-description
|
||||||
|
text_level, text_techs = analyze_proficiency_text(proficiency_self + " " + project_reason)
|
||||||
|
result['self_described_level'] = text_level
|
||||||
|
result['languages_from_text'] = text_techs
|
||||||
|
|
||||||
|
# Fetch GitHub data if URL provided
|
||||||
|
if project_url and 'github.com' in project_url:
|
||||||
|
owner, repo = extract_github_info(project_url)
|
||||||
|
|
||||||
|
if owner:
|
||||||
|
result['github_username'] = owner
|
||||||
|
|
||||||
|
# Fetch user profile
|
||||||
|
user_data = fetch_github_user(owner)
|
||||||
|
if user_data:
|
||||||
|
result['github_repos_count'] = user_data.get('public_repos', 0)
|
||||||
|
result['github_followers'] = user_data.get('followers', 0)
|
||||||
|
|
||||||
|
# Fetch repos to get languages
|
||||||
|
repos = fetch_github_repos(owner)
|
||||||
|
all_languages = set()
|
||||||
|
for r in repos[:10]: # Check top 10 repos
|
||||||
|
if r.get('language'):
|
||||||
|
all_languages.add(r['language'].lower())
|
||||||
|
result['languages_from_github'] = list(all_languages)
|
||||||
|
|
||||||
|
# If specific repo provided, get its languages
|
||||||
|
if repo:
|
||||||
|
repo_langs = fetch_repo_languages(owner, repo)
|
||||||
|
for lang in repo_langs.keys():
|
||||||
|
all_languages.add(lang.lower())
|
||||||
|
result['languages_from_github'] = list(all_languages)
|
||||||
|
|
||||||
|
time.sleep(0.5) # Rate limiting
|
||||||
|
|
||||||
|
# Combine tech stack
|
||||||
|
all_tech = set(result['languages_from_github']) | set(result['languages_from_text'])
|
||||||
|
result['tech_stack'] = sorted(list(all_tech))
|
||||||
|
|
||||||
|
# Determine final proficiency
|
||||||
|
# Factors: self-description, GitHub activity, tech diversity
|
||||||
|
github_score = 0
|
||||||
|
if result['github_repos_count'] >= 20:
|
||||||
|
github_score += 2
|
||||||
|
elif result['github_repos_count'] >= 10:
|
||||||
|
github_score += 1
|
||||||
|
|
||||||
|
if result['github_followers'] >= 50:
|
||||||
|
github_score += 2
|
||||||
|
elif result['github_followers'] >= 10:
|
||||||
|
github_score += 1
|
||||||
|
|
||||||
|
tech_count = len(result['tech_stack'])
|
||||||
|
if tech_count >= 6:
|
||||||
|
github_score += 2
|
||||||
|
elif tech_count >= 3:
|
||||||
|
github_score += 1
|
||||||
|
|
||||||
|
# Map self-described level to score
|
||||||
|
level_scores = {'beginner': 0, 'intermediate': 2, 'advanced': 4}
|
||||||
|
self_score = level_scores.get(text_level, 2)
|
||||||
|
|
||||||
|
# Combined score
|
||||||
|
total_score = github_score + self_score
|
||||||
|
|
||||||
|
if total_score >= 7:
|
||||||
|
result['final_proficiency'] = 'advanced'
|
||||||
|
elif total_score >= 3:
|
||||||
|
result['final_proficiency'] = 'intermediate'
|
||||||
|
else:
|
||||||
|
result['final_proficiency'] = 'beginner'
|
||||||
|
|
||||||
|
# Add notes
|
||||||
|
if not project_url or 'github.com' not in project_url:
|
||||||
|
result['notes'].append('No GitHub URL provided')
|
||||||
|
if result['github_repos_count'] == 0 and result['github_username']:
|
||||||
|
result['notes'].append('GitHub profile has no public repos')
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Load applicants
|
||||||
|
with open('applicants_to_evaluate.json', 'r') as f:
|
||||||
|
applicants = json.load(f)
|
||||||
|
|
||||||
|
print(f"Evaluating {len(applicants)} applicants...\n")
|
||||||
|
|
||||||
|
evaluations = []
|
||||||
|
for i, applicant in enumerate(applicants):
|
||||||
|
result = evaluate_applicant(applicant, i, len(applicants))
|
||||||
|
evaluations.append(result)
|
||||||
|
|
||||||
|
# Progress update every 10
|
||||||
|
if (i + 1) % 10 == 0:
|
||||||
|
print(f" Progress: {i+1}/{len(applicants)} complete")
|
||||||
|
|
||||||
|
# Save results
|
||||||
|
with open('proficiency_evaluations.json', 'w') as f:
|
||||||
|
json.dump(evaluations, f, indent=2)
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
beginner = sum(1 for e in evaluations if e['final_proficiency'] == 'beginner')
|
||||||
|
intermediate = sum(1 for e in evaluations if e['final_proficiency'] == 'intermediate')
|
||||||
|
advanced = sum(1 for e in evaluations if e['final_proficiency'] == 'advanced')
|
||||||
|
|
||||||
|
print(f"\n=== EVALUATION COMPLETE ===")
|
||||||
|
print(f"Beginner: {beginner}")
|
||||||
|
print(f"Intermediate: {intermediate}")
|
||||||
|
print(f"Advanced: {advanced}")
|
||||||
|
print(f"Total: {len(evaluations)}")
|
||||||
|
print(f"\nResults saved to proficiency_evaluations.json")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
[project]
|
||||||
|
name = "ephemere"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Collection of ephemeral scripts"
|
||||||
|
authors = [
|
||||||
|
{ name = "Naomi Carrigan", email = "nhcarrigan@gmail.com" }
|
||||||
|
]
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"ruff==0.14.14"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py310"
|
||||||
|
line-length = 88
|
||||||
|
indent-width = 4
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
# pycodestyle
|
||||||
|
"E",
|
||||||
|
# pyflakes
|
||||||
|
"F",
|
||||||
|
# isort
|
||||||
|
"I",
|
||||||
|
# pydocstyle
|
||||||
|
"D",
|
||||||
|
# pyupgrade
|
||||||
|
"UP",
|
||||||
|
# flake8-bugbear
|
||||||
|
"B",
|
||||||
|
# flake8-comprehensions
|
||||||
|
"C4",
|
||||||
|
# flake8-datetimez
|
||||||
|
"DTZ",
|
||||||
|
# flake8-implicit-str-concat
|
||||||
|
"ISC",
|
||||||
|
# flake8-logging-format
|
||||||
|
"G",
|
||||||
|
# flake8-print
|
||||||
|
"T20",
|
||||||
|
# flake8-pytest-style
|
||||||
|
"PT",
|
||||||
|
# flake8-quotes
|
||||||
|
"Q",
|
||||||
|
# flake8-simplify
|
||||||
|
"SIM",
|
||||||
|
# flake8-tidy-imports
|
||||||
|
"TID",
|
||||||
|
# pylint
|
||||||
|
"PL",
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
# Missing docstrings
|
||||||
|
"D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107",
|
||||||
|
# Let's not require module docstrings for scripts
|
||||||
|
"D100",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.pydocstyle]
|
||||||
|
convention = "google"
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
known-first-party = ["py"]
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "space"
|
||||||
|
skip-magic-trailing-comma = false
|
||||||
|
line-ending = "auto"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Development dependencies
|
||||||
|
ruff==0.14.14
|
||||||
Generated
+43
@@ -0,0 +1,43 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ephemere"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [{ name = "ruff", marker = "extra == 'dev'", specifier = "==0.14.14" }]
|
||||||
|
provides-extras = ["dev"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.14.14"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" },
|
||||||
|
]
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Colors and formatting
|
||||||
|
PINK='\033[38;5;213m'
|
||||||
|
BLUE='\033[38;5;117m'
|
||||||
|
GREEN='\033[38;5;156m'
|
||||||
|
YELLOW='\033[38;5;229m'
|
||||||
|
CYAN='\033[38;5;123m'
|
||||||
|
WHITE='\033[1;37m'
|
||||||
|
DIM='\033[2m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
RESET='\033[0m'
|
||||||
|
|
||||||
|
# Box drawing characters
|
||||||
|
TOP_LEFT='╭'
|
||||||
|
TOP_RIGHT='╮'
|
||||||
|
BOTTOM_LEFT='╰'
|
||||||
|
BOTTOM_RIGHT='╯'
|
||||||
|
HORIZONTAL='─'
|
||||||
|
VERTICAL='│'
|
||||||
|
ARROW='❯'
|
||||||
|
SPARKLE='✨'
|
||||||
|
STAR='★'
|
||||||
|
|
||||||
|
# Clear screen and show header
|
||||||
|
clear
|
||||||
|
echo -e "${PINK}${BOLD} ${TOP_LEFT}────────────────────────────────────${TOP_RIGHT}"
|
||||||
|
echo -e " ${VERTICAL} ${SPARKLE} ${WHITE}Ephemere Script Runner${PINK} ${SPARKLE} ${VERTICAL}"
|
||||||
|
echo -e " ${BOTTOM_LEFT}────────────────────────────────────${BOTTOM_RIGHT}${RESET}"
|
||||||
|
|
||||||
|
# Function to display a menu and get selection
|
||||||
|
select_option() {
|
||||||
|
local prompt="$1"
|
||||||
|
shift
|
||||||
|
local options=("$@")
|
||||||
|
local selected=0
|
||||||
|
local key
|
||||||
|
|
||||||
|
# Hide cursor
|
||||||
|
tput civis
|
||||||
|
|
||||||
|
# Trap to restore cursor on exit
|
||||||
|
trap 'tput cnorm' EXIT
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
# Clear previous menu (move up and clear lines)
|
||||||
|
if [ -n "$menu_drawn" ]; then
|
||||||
|
for ((i=0; i<=${#options[@]}+1; i++)); do
|
||||||
|
tput cuu1
|
||||||
|
tput el
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
menu_drawn=1
|
||||||
|
|
||||||
|
# Print prompt
|
||||||
|
echo -e "${CYAN}${BOLD} $prompt${RESET}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Print options
|
||||||
|
for i in "${!options[@]}"; do
|
||||||
|
if [ $i -eq $selected ]; then
|
||||||
|
echo -e " ${PINK}${BOLD}$ARROW ${WHITE}${options[$i]}${RESET}"
|
||||||
|
else
|
||||||
|
echo -e " ${DIM}${options[$i]}${RESET}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Read a single keypress
|
||||||
|
read -rsn1 key
|
||||||
|
|
||||||
|
# Handle arrow keys (they come as escape sequences)
|
||||||
|
if [[ $key == $'\x1b' ]]; then
|
||||||
|
read -rsn2 key
|
||||||
|
case $key in
|
||||||
|
'[A') # Up arrow
|
||||||
|
((selected--))
|
||||||
|
[ $selected -lt 0 ] && selected=$((${#options[@]}-1))
|
||||||
|
;;
|
||||||
|
'[B') # Down arrow
|
||||||
|
((selected++))
|
||||||
|
[ $selected -ge ${#options[@]} ] && selected=0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
elif [[ $key == "" ]]; then
|
||||||
|
# Enter pressed
|
||||||
|
tput cnorm # Show cursor
|
||||||
|
unset menu_drawn
|
||||||
|
return $selected
|
||||||
|
elif [[ $key == "q" || $key == "Q" ]]; then
|
||||||
|
tput cnorm
|
||||||
|
echo -e "\n${YELLOW} Bye bye! $SPARKLE${RESET}\n"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 1: Select Language
|
||||||
|
echo ""
|
||||||
|
languages=("TypeScript" "Python")
|
||||||
|
select_option "Select a language:" "${languages[@]}"
|
||||||
|
lang_index=$?
|
||||||
|
language="${languages[$lang_index]}"
|
||||||
|
|
||||||
|
echo -e "\n ${GREEN}$STAR Selected: ${WHITE}$language${RESET}\n"
|
||||||
|
|
||||||
|
# Step 2: Get categories based on language
|
||||||
|
if [ "$language" == "TypeScript" ]; then
|
||||||
|
script_dir="typescript/src"
|
||||||
|
runner="pnpm tsx"
|
||||||
|
# Get subdirectories as categories (excluding utils and interfaces)
|
||||||
|
mapfile -t categories < <(find "$script_dir" -mindepth 1 -maxdepth 1 -type d ! -name 'utils' ! -name 'interfaces' -exec basename {} \; | sort)
|
||||||
|
else
|
||||||
|
script_dir="python"
|
||||||
|
runner="uv run python"
|
||||||
|
# Get subdirectories as categories (excluding __pycache__ and .venv)
|
||||||
|
mapfile -t categories < <(find "$script_dir" -mindepth 1 -maxdepth 1 -type d ! -name '__pycache__' ! -name '.venv' ! -name '*.egg-info' -exec basename {} \; | sort)
|
||||||
|
# Add "Root Scripts" option for Python files in root
|
||||||
|
if ls "$script_dir"/*.py &>/dev/null 2>&1; then
|
||||||
|
categories=("Root Scripts" "${categories[@]}")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#categories[@]} -eq 0 ]; then
|
||||||
|
echo -e " ${YELLOW}No script categories found!${RESET}\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
select_option "Select a category:" "${categories[@]}"
|
||||||
|
cat_index=$?
|
||||||
|
category="${categories[$cat_index]}"
|
||||||
|
|
||||||
|
echo -e "\n ${GREEN}$STAR Selected: ${WHITE}$category${RESET}\n"
|
||||||
|
|
||||||
|
# Step 3: Get scripts in category
|
||||||
|
if [ "$category" == "Root Scripts" ]; then
|
||||||
|
search_dir="$script_dir"
|
||||||
|
mapfile -t scripts < <(find "$search_dir" -maxdepth 1 -name "*.py" -exec basename {} \; | sort)
|
||||||
|
elif [ "$language" == "TypeScript" ]; then
|
||||||
|
search_dir="$script_dir/$category"
|
||||||
|
mapfile -t scripts < <(find "$search_dir" -name "*.ts" -exec basename {} \; | sort)
|
||||||
|
else
|
||||||
|
search_dir="$script_dir/$category"
|
||||||
|
mapfile -t scripts < <(find "$search_dir" -name "*.py" ! -name "__init__.py" -exec basename {} \; | sort)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#scripts[@]} -eq 0 ]; then
|
||||||
|
echo -e " ${YELLOW}No scripts found in this category!${RESET}\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
select_option "Select a script:" "${scripts[@]}"
|
||||||
|
script_index=$?
|
||||||
|
script="${scripts[$script_index]}"
|
||||||
|
|
||||||
|
echo -e "\n ${GREEN}$STAR Selected: ${WHITE}$script${RESET}\n"
|
||||||
|
|
||||||
|
# Build the full script path
|
||||||
|
if [ "$category" == "Root Scripts" ]; then
|
||||||
|
script_path="$script"
|
||||||
|
elif [ "$language" == "TypeScript" ]; then
|
||||||
|
script_path="src/$category/$script"
|
||||||
|
else
|
||||||
|
script_path="$category/$script"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show what we're about to run
|
||||||
|
echo -e "${BLUE}${BOLD} ${TOP_LEFT}───────────────────────${TOP_RIGHT}"
|
||||||
|
echo -e " ${VERTICAL} ${WHITE}Running script...${BLUE} ${VERTICAL}"
|
||||||
|
echo -e " ${BOTTOM_LEFT}───────────────────────${BOTTOM_RIGHT}${RESET}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${DIM}Language:${RESET} $language"
|
||||||
|
echo -e " ${DIM}Category:${RESET} $category"
|
||||||
|
echo -e " ${DIM}Script:${RESET} $script"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run the script with 1Password env injection
|
||||||
|
if [ "$language" == "TypeScript" ]; then
|
||||||
|
cd typescript
|
||||||
|
echo -e " ${DIM}$ op run --env-file=../prod.env -- $runner $script_path${RESET}\n"
|
||||||
|
op run --env-file=../prod.env --no-masking -- $runner "$script_path"
|
||||||
|
else
|
||||||
|
cd python
|
||||||
|
echo -e " ${DIM}$ op run --env-file=../prod.env -- $runner $script_path${RESET}\n"
|
||||||
|
op run --env-file=../prod.env --no-masking -- $runner "$script_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit_code=$?
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [ $exit_code -eq 0 ]; then
|
||||||
|
echo -e " ${GREEN}${BOLD}$SPARKLE Script completed successfully! $SPARKLE${RESET}\n"
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}${BOLD}Script exited with code $exit_code${RESET}\n"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
.PHONY: help install build lint test clean
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "TypeScript project commands:"
|
||||||
|
@echo " make install - Install dependencies"
|
||||||
|
@echo " make build - Build TypeScript (type check)"
|
||||||
|
@echo " make lint - Run ESLint"
|
||||||
|
@echo " make test - Run tests"
|
||||||
|
@echo " make clean - Clean build artifacts"
|
||||||
|
|
||||||
|
install:
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
build:
|
||||||
|
pnpm exec tsc --noEmit
|
||||||
|
|
||||||
|
lint:
|
||||||
|
pnpm exec eslint src --max-warnings 0
|
||||||
|
|
||||||
|
test:
|
||||||
|
@echo "No tests configured yet"
|
||||||
|
@exit 0
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf node_modules
|
||||||
|
rm -rf dist
|
||||||
|
rm -f *.tsbuildinfo
|
||||||
@@ -5,10 +5,7 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --noEmit",
|
"start": "op run --env-file=prod.env --no-masking -- tsx"
|
||||||
"lint": "eslint src --max-warnings 0",
|
|
||||||
"start": "op run --env-file=prod.env --no-masking -- tsx",
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 0"
|
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import { select } from "@inquirer/prompts";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { readdirSync, statSync, existsSync } from "fs";
|
||||||
|
import { join, relative } from "path";
|
||||||
|
|
||||||
|
interface ScriptOption {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeScriptCategories = (): string[] => {
|
||||||
|
const srcPath = join(__dirname, "..");
|
||||||
|
const entries = readdirSync(srcPath);
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.filter((entry) => {
|
||||||
|
const fullPath = join(srcPath, entry);
|
||||||
|
return statSync(fullPath).isDirectory() && entry !== "utils" && entry !== "interfaces";
|
||||||
|
})
|
||||||
|
.sort();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTypeScriptScripts = (category: string): ScriptOption[] => {
|
||||||
|
const categoryPath = join(__dirname, "..", category);
|
||||||
|
const scripts: ScriptOption[] = [];
|
||||||
|
|
||||||
|
const walkDirectory = (dir: string) => {
|
||||||
|
const entries = readdirSync(dir);
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = join(dir, entry);
|
||||||
|
const stat = statSync(fullPath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
walkDirectory(fullPath);
|
||||||
|
} else if (entry.endsWith(".ts") && entry !== "index.ts") {
|
||||||
|
const relativePath = relative(join(__dirname, ".."), fullPath);
|
||||||
|
scripts.push({
|
||||||
|
name: entry.replace(".ts", ""),
|
||||||
|
value: relativePath,
|
||||||
|
description: relativePath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
walkDirectory(categoryPath);
|
||||||
|
return scripts.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPythonCategories = (): string[] => {
|
||||||
|
const pythonPath = join(__dirname, "../../../../python");
|
||||||
|
const entries = readdirSync(pythonPath);
|
||||||
|
|
||||||
|
const categories = entries
|
||||||
|
.filter((entry) => {
|
||||||
|
const fullPath = join(pythonPath, entry);
|
||||||
|
return statSync(fullPath).isDirectory() &&
|
||||||
|
!entry.startsWith(".") &&
|
||||||
|
entry !== "__pycache__";
|
||||||
|
})
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
// Also check for scripts in the root
|
||||||
|
const hasRootScripts = entries.some(entry => entry.endsWith(".py"));
|
||||||
|
if (hasRootScripts) {
|
||||||
|
categories.unshift("(root)");
|
||||||
|
}
|
||||||
|
|
||||||
|
return categories;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPythonScripts = (category: string): ScriptOption[] => {
|
||||||
|
const pythonPath = join(__dirname, "../../../../python");
|
||||||
|
const searchPath = category === "(root)" ? pythonPath : join(pythonPath, category);
|
||||||
|
|
||||||
|
const scripts: ScriptOption[] = [];
|
||||||
|
const entries = readdirSync(searchPath);
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.endsWith(".py") && !entry.startsWith("__")) {
|
||||||
|
const relativePath = category === "(root)" ? entry : join(category, entry);
|
||||||
|
scripts.push({
|
||||||
|
name: entry.replace(".py", ""),
|
||||||
|
value: relativePath,
|
||||||
|
description: relativePath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scripts.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
};
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
console.log("🌸 Welcome to Ephemere Script Runner! 💖\n");
|
||||||
|
|
||||||
|
// Select language
|
||||||
|
const language = await select({
|
||||||
|
message: "Which language would you like to run?",
|
||||||
|
choices: [
|
||||||
|
{ name: "TypeScript", value: "typescript", description: "Run a TypeScript script" },
|
||||||
|
{ name: "Python", value: "python", description: "Run a Python script" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get categories based on language
|
||||||
|
const categories = language === "typescript"
|
||||||
|
? getTypeScriptCategories()
|
||||||
|
: getPythonCategories();
|
||||||
|
|
||||||
|
if (categories.length === 0) {
|
||||||
|
console.error(`No categories found for ${language}!`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select category
|
||||||
|
const category = await select({
|
||||||
|
message: "Which category?",
|
||||||
|
choices: categories.map(cat => ({
|
||||||
|
name: cat === "(root)" ? "Root Directory" : cat.charAt(0).toUpperCase() + cat.slice(1),
|
||||||
|
value: cat,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get scripts for selected category
|
||||||
|
const scripts = language === "typescript"
|
||||||
|
? getTypeScriptScripts(category)
|
||||||
|
: getPythonScripts(category);
|
||||||
|
|
||||||
|
if (scripts.length === 0) {
|
||||||
|
console.error(`No scripts found in ${category}!`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select script
|
||||||
|
const script = await select({
|
||||||
|
message: "Which script would you like to run?",
|
||||||
|
choices: scripts,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build and execute the command
|
||||||
|
const prodEnvPath = join(__dirname, "../../../../../prod.env");
|
||||||
|
let command: string;
|
||||||
|
|
||||||
|
if (language === "typescript") {
|
||||||
|
command = `cd ${join(__dirname, "../../../")} && op run --env-file=${prodEnvPath} -- pnpm exec tsx src/${script}`;
|
||||||
|
} else {
|
||||||
|
command = `cd ${join(__dirname, "../../../../python")} && op run --env-file=${prodEnvPath} -- uv run python ${script}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n✨ Running: ${script}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(command, {
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("\n❌ Script execution failed!");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
Reference in New Issue
Block a user