Initial commit: MISF VOB download and upload automation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
c2dec96b1f
43 files changed
+10944
No files matched your search
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Create Input/input.xlsx (if missing) and refresh Input/input.xlsx.example.
|
||||
|
||||
Run from repository root:
|
||||
python scripts/bootstrap_input_xlsx.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
INPUT_DIR = PROJECT_ROOT / "Input"
|
||||
CONFIG_PATH = INPUT_DIR / "input.xlsx"
|
||||
EXAMPLE_PATH = INPUT_DIR / "input.xlsx.example"
|
||||
|
||||
FACILITY_KEYWORDS = [
|
||||
"amaya", "brighton place san diego", "brighton place sd", "brighton place spring valley",
|
||||
"brighton place-spring valley", "granite hill", "anberry", "arbor hills", "bayberry",
|
||||
"bradley court", "castle manor", "friendship", "gramercy", "kearny", "lakeside",
|
||||
"newport", "siena", "stanford court", "temecula", "walnut", "balboa", "cottonwood",
|
||||
"country hills", "la mesa", "university care", "blue oak", "bridgewood", "capistrano",
|
||||
"eden", "hillcrest heights", "la casa via", "torrey pines", "bradley gardens", "horizon",
|
||||
"community care", "jacob", "the shores", "valley vista", "valle vista", "golden hill",
|
||||
"grossmont post acute", "lemon grove", "mission hills", "parkside", "somerset",
|
||||
"the springs", "victoria", "reo vista", "san diego post acute", "villa las palmas",
|
||||
"ridgeview", "windsor gardens", "windsor rosewood",
|
||||
]
|
||||
FACILITIES = [k.title() for k in FACILITY_KEYWORDS]
|
||||
FACILITIES.append("REPLACE_WITH_54TH_FACILITY_NAME")
|
||||
|
||||
DEFAULT_PLANNER_URL = (
|
||||
"https://planner.cloud.microsoft/webui/mytasks/assignedtome/view/grid"
|
||||
"?tid=0c15ad2c-19dd-4ded-9cd1-20d344386414"
|
||||
)
|
||||
|
||||
SHEET1_ROWS = [
|
||||
("Proxy IP", ""),
|
||||
("Proxy Port", ""),
|
||||
("Proxy Username", ""),
|
||||
("Proxy Password", ""),
|
||||
("OneDrive Username", ""),
|
||||
("OneDrive Password", ""),
|
||||
("Microsoft TOTP Secret", ""),
|
||||
("Planner URL", DEFAULT_PLANNER_URL),
|
||||
(
|
||||
"OneDrive URL",
|
||||
"https://netorg393890-my.sharepoint.com/personal/joy_accesspsychology_org"
|
||||
"/_layouts/15/onedrive.aspx?id=%2Fpersonal%2Fjoy%5Faccesspsychology%5Forg%2FDocuments"
|
||||
"%2FAccess%20Facility%20Spreadsheets3",
|
||||
),
|
||||
("pdf_folder_path", str(PROJECT_ROOT / "Input" / "PDFs")),
|
||||
]
|
||||
|
||||
|
||||
def _build_workbook() -> tuple[pd.DataFrame, pd.DataFrame]:
|
||||
sheet1 = pd.DataFrame(SHEET1_ROWS, columns=["Key", "Value"])
|
||||
facilities = pd.DataFrame({"Facility Name": FACILITIES})
|
||||
return sheet1, facilities
|
||||
|
||||
|
||||
def _write(path: Path, sheet1: pd.DataFrame, facilities: pd.DataFrame) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with pd.ExcelWriter(path, engine="openpyxl") as writer:
|
||||
sheet1.to_excel(writer, sheet_name="Sheet1", index=False, header=False)
|
||||
facilities.to_excel(writer, sheet_name="Facilities", index=False)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
sheet1, facilities = _build_workbook()
|
||||
|
||||
_write(EXAMPLE_PATH, sheet1, facilities)
|
||||
print(f"Wrote template -> {EXAMPLE_PATH} ({len(FACILITIES)} facilities)")
|
||||
|
||||
if CONFIG_PATH.exists():
|
||||
print(f"Skipped (already exists): {CONFIG_PATH}")
|
||||
return
|
||||
|
||||
_write(CONFIG_PATH, sheet1, facilities)
|
||||
print(f"Created local config -> {CONFIG_PATH}")
|
||||
print("Edit input.xlsx with your credentials before running run_download.py")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,88 @@
|
||||
# Build MISF_VOB_Download.exe and install at project root (double-click to run).
|
||||
# Output: <project root>\MISF_VOB_Download.exe + _internal\
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location $Root
|
||||
|
||||
Write-Host "=== MISF VOB Download - production build (project root) ===" -ForegroundColor Cyan
|
||||
|
||||
$PortableChrome = Join-Path $Root "chrome-win\chrome-win\chrome.exe"
|
||||
$PortableDriver = Join-Path $Root "chrome-win\chrome-win\chromedriver.exe"
|
||||
if (-not (Test-Path $PortableChrome)) {
|
||||
throw "Portable Chromium not found: $PortableChrome (required for production build)"
|
||||
}
|
||||
if (-not (Test-Path $PortableDriver)) {
|
||||
Write-Warning "Bundled chromedriver missing: $PortableDriver (will download at runtime)"
|
||||
} else {
|
||||
Write-Host "Using portable Chromium: $PortableChrome"
|
||||
}
|
||||
|
||||
if (-not (Test-Path ".venv\Scripts\python.exe")) {
|
||||
Write-Host "Creating virtual environment..."
|
||||
python -m venv .venv
|
||||
}
|
||||
|
||||
$Python = Join-Path $Root ".venv\Scripts\python.exe"
|
||||
& $Python -m pip install --upgrade pip -q
|
||||
& $Python -m pip install -r requirements.txt -q
|
||||
& $Python -m pip install "pyinstaller>=6.10" -q
|
||||
|
||||
Write-Host "Running PyInstaller..."
|
||||
& $Python -m PyInstaller --noconfirm MISF_VOB_Download.spec
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "PyInstaller failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
$BuiltExe = Join-Path $Root "dist\MISF_VOB_Download.exe"
|
||||
$BuiltDir = Join-Path $Root "dist\MISF_VOB_Download"
|
||||
$BuiltDirExe = Join-Path $BuiltDir "MISF_VOB_Download.exe"
|
||||
|
||||
if (Test-Path $BuiltExe) {
|
||||
Write-Host "Installing one-file build to project root..."
|
||||
$ExeDest = Join-Path $Root "MISF_VOB_Download.exe"
|
||||
Get-Process -Name "MISF_VOB_Download" -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||
Start-Sleep -Seconds 1
|
||||
Copy-Item $BuiltExe $ExeDest -Force
|
||||
Remove-Item $BuiltExe -Force
|
||||
} elseif (Test-Path $BuiltDirExe) {
|
||||
Write-Host "Installing folder build to project root..."
|
||||
$ExeDest = Join-Path $Root "MISF_VOB_Download.exe"
|
||||
Get-Process -Name "MISF_VOB_Download" -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||
Start-Sleep -Seconds 1
|
||||
$InternalDest = Join-Path $Root "_internal"
|
||||
|
||||
Copy-Item $BuiltDirExe $ExeDest -Force
|
||||
if (Test-Path $InternalDest) {
|
||||
Remove-Item $InternalDest -Recurse -Force
|
||||
}
|
||||
Move-Item (Join-Path $BuiltDir "_internal") $InternalDest -Force
|
||||
Remove-Item $BuiltDir -Recurse -Force
|
||||
} else {
|
||||
throw "Expected build output not found: $BuiltExe or $BuiltDirExe"
|
||||
}
|
||||
$DistFolder = Join-Path $Root "dist"
|
||||
if (Test-Path $DistFolder) {
|
||||
$remaining = @(Get-ChildItem $DistFolder -Force -ErrorAction SilentlyContinue)
|
||||
if ($remaining.Count -eq 0) {
|
||||
Remove-Item $DistFolder -Force
|
||||
}
|
||||
}
|
||||
|
||||
$InputDir = Join-Path $Root "Input"
|
||||
$OutputDir = Join-Path $Root "Output"
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $InputDir "PDFs") | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null
|
||||
Copy-Item (Join-Path $Root "docs\PRODUCTION_README.txt") (Join-Path $Root "README_PRODUCTION.txt") -Force
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Build complete." -ForegroundColor Green
|
||||
Write-Host " Run: $ExeDest"
|
||||
Write-Host ""
|
||||
Write-Host "First-time setup:" -ForegroundColor Yellow
|
||||
Write-Host " 1. Copy Input\input.xlsx.example to Input\input.xlsx and fill credentials"
|
||||
Write-Host " 2. Add facility names on the Facilities sheet"
|
||||
Write-Host " 3. Keep chrome-win\ next to the .exe"
|
||||
Write-Host " 4. Double-click MISF_VOB_Download.exe in this folder"
|
||||
Write-Host ""
|
||||
Write-Host 'See README_PRODUCTION.txt. Exe is at project root (no dist folder).'
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Print config status (no browser). Run: python scripts/validate_config.py"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from config import ( # noqa: E402
|
||||
CONFIG_FILE,
|
||||
DEFAULT_PLANNER_URL,
|
||||
EXPECTED_FACILITY_COUNT,
|
||||
credentials_setup_message,
|
||||
missing_onedrive_credentials,
|
||||
read_app_config,
|
||||
read_facility_list,
|
||||
)
|
||||
from totp_utils import validate_totp_secret # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
path = CONFIG_FILE if CONFIG_FILE.exists() else ROOT / "Input" / "input.xlsx.example"
|
||||
if not path.exists():
|
||||
print(f"[FAIL] Missing {path}")
|
||||
print(" copy Input\\input.xlsx.example Input\\input.xlsx")
|
||||
return 1
|
||||
|
||||
cfg = read_app_config(path)
|
||||
missing = missing_onedrive_credentials(
|
||||
cfg.onedrive_username,
|
||||
cfg.onedrive_password,
|
||||
cfg.microsoft_totp_secret,
|
||||
)
|
||||
if missing and path == CONFIG_FILE:
|
||||
print("[FAIL] Credentials not configured:")
|
||||
print(credentials_setup_message(missing))
|
||||
return 1
|
||||
|
||||
try:
|
||||
validate_totp_secret(cfg.microsoft_totp_secret or "JBSWY3DPEHPK3PXP")
|
||||
except ValueError as exc:
|
||||
if cfg.microsoft_totp_secret:
|
||||
print(f"[FAIL] TOTP secret: {exc}")
|
||||
return 1
|
||||
print(f"[WARN] TOTP not set — format check skipped ({exc})")
|
||||
|
||||
facilities = read_facility_list(path, warn_if_not_expected_count=True)
|
||||
planner = cfg.planner_url or DEFAULT_PLANNER_URL
|
||||
if not planner.startswith("https://"):
|
||||
print(f"[FAIL] Planner URL must be https — got: {planner[:80]}")
|
||||
return 1
|
||||
|
||||
print(f"[OK] Config file: {path.name}")
|
||||
if cfg.onedrive_username:
|
||||
print(f"[OK] Credentials set for {cfg.onedrive_username}")
|
||||
else:
|
||||
print("[OK] Template mode (no credentials in example file)")
|
||||
print(f"[OK] Planner URL: {planner[:70]}...")
|
||||
print(f"[OK] PDF folder: {cfg.pdf_folder_path or '(default Input/PDFs)'}")
|
||||
print(f"[OK] {len(facilities)} facilities (expected {EXPECTED_FACILITY_COUNT})")
|
||||
if len(facilities) != EXPECTED_FACILITY_COUNT:
|
||||
print("[WARN] Facility count differs from expected 54 — verify Facilities sheet")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,71 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Pre-flight validation for UiPath MISF Planner Download pilot (mirrors Python validate_config + output checks).
|
||||
#>
|
||||
param(
|
||||
[string]$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path,
|
||||
[int]$ExpectedFacilities = 54,
|
||||
[switch]$CheckOutput
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$configPath = Join-Path $ProjectRoot "Input\input.xlsx"
|
||||
$uipathProject = Join-Path $ProjectRoot "uipath\MISF_Planner_Download\project.json"
|
||||
$today = Get-Date -Format "yyyy-MM-dd"
|
||||
|
||||
Write-Host "MISF UiPath pilot validation"
|
||||
Write-Host "Project root: $ProjectRoot"
|
||||
|
||||
if (-not (Test-Path $configPath)) {
|
||||
Write-Error "Missing config: $configPath (copy Input\input.xlsx.example)"
|
||||
}
|
||||
if (-not (Test-Path $uipathProject)) {
|
||||
Write-Error "Missing UiPath project: $uipathProject"
|
||||
}
|
||||
|
||||
# Run Python config validator if available
|
||||
$pyValidate = Join-Path $ProjectRoot "scripts\validate_config.py"
|
||||
if (Test-Path $pyValidate) {
|
||||
Write-Host "Running Python validate_config.py..."
|
||||
python $pyValidate
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
# Verify UiPath workflow files exist
|
||||
$requiredWorkflows = @(
|
||||
"Main.xaml",
|
||||
"01_ReadConfig.xaml",
|
||||
"02_LoginPlanner.xaml",
|
||||
"03_ProcessFacility.xaml",
|
||||
"04_DownloadTaskAttachments.xaml",
|
||||
"05_SetTaskInProgress.xaml",
|
||||
"06_WriteSummary.xaml",
|
||||
"Helpers\GenerateTotp.xaml",
|
||||
"Helpers\SanitizeFolderName.xaml",
|
||||
"Selectors\PlannerSelectors.json"
|
||||
)
|
||||
$uipathDir = Join-Path $ProjectRoot "uipath\MISF_Planner_Download"
|
||||
foreach ($wf in $requiredWorkflows) {
|
||||
$p = Join-Path $uipathDir $wf
|
||||
if (-not (Test-Path $p)) {
|
||||
Write-Error "Missing workflow file: $p"
|
||||
}
|
||||
}
|
||||
Write-Host "All UiPath workflow files present."
|
||||
|
||||
if ($CheckOutput) {
|
||||
$summary = Join-Path $ProjectRoot "Output\$today\Summary\Download_Summary_$today.xlsx"
|
||||
$original = Join-Path $ProjectRoot "Output\$today\Original"
|
||||
if (-not (Test-Path $summary)) {
|
||||
Write-Warning "No summary yet: $summary (run UiPath Main with in_FacilityLimit=2)"
|
||||
} else {
|
||||
Write-Host "Summary found: $summary"
|
||||
}
|
||||
if (Test-Path $original) {
|
||||
$pdfCount = (Get-ChildItem -Path $original -Recurse -Filter "*.pdf" -ErrorAction SilentlyContinue).Count
|
||||
Write-Host "PDFs under Original: $pdfCount"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Pilot pre-flight OK."
|
||||
Reference in new issue
Block a user