chore: adiciona .gitignore e commit.command
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import Foundation
|
||||
|
||||
/// Bridges the SwiftUI app to the Python engine via the `admin/models_api.py`
|
||||
/// JSON bridge. The app launches the script as a subprocess and reads
|
||||
/// newline-delimited JSON on stdout (JSON-lines), so long-running commands can
|
||||
/// stream progress.
|
||||
enum PythonBridge {
|
||||
|
||||
/// Resolve the Python interpreter + script path.
|
||||
/// Prefers `uv run` so extras are available; falls back to `python3`.
|
||||
static var scriptURL: URL {
|
||||
// Search order: cwd + ancestors, the app's own bundle root, home, and
|
||||
// some common mount points (the repo often lives under /Volumes/...).
|
||||
let cwd = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
|
||||
var searchRoots: [URL] = []
|
||||
var dir = cwd
|
||||
for _ in 0..<8 { searchRoots.append(dir); dir.deleteLastPathComponent() }
|
||||
if let bundle = Bundle.main.resourceURL {
|
||||
searchRoots.append(bundle)
|
||||
}
|
||||
searchRoots.append(URL(fileURLWithPath: NSHomeDirectory()))
|
||||
searchRoots.append(URL(fileURLWithPath: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/G-ART"))
|
||||
|
||||
for root in searchRoots {
|
||||
let candidate = root.appendingPathComponent("admin/models_api.py")
|
||||
if FileManager.default.fileExists(atPath: candidate.path) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
// Fall back to cwd.
|
||||
return cwd.appendingPathComponent("admin/models_api.py")
|
||||
}
|
||||
|
||||
/// Project root containing `admin/`.
|
||||
static var projectRoot: URL {
|
||||
scriptURL.deletingLastPathComponent().deletingLastPathComponent()
|
||||
}
|
||||
|
||||
static var pythonCommand: [String] {
|
||||
// Prefer `uv run python` (has extras); fall back to `python3`.
|
||||
if let uv = findUV() {
|
||||
return [uv, "run", "python", scriptURL.path]
|
||||
}
|
||||
return ["python3", scriptURL.path]
|
||||
}
|
||||
|
||||
static var workingDirectory: URL {
|
||||
projectRoot
|
||||
}
|
||||
|
||||
/// Locate `uv` on PATH or in common install locations.
|
||||
private static func findUV() -> String? {
|
||||
if let onPath = which("uv") { return onPath }
|
||||
let candidates = [
|
||||
"/usr/local/bin/uv",
|
||||
"/opt/homebrew/bin/uv",
|
||||
"/opt/local/bin/uv",
|
||||
"\(NSHomeDirectory())/.local/bin/uv",
|
||||
"\(NSHomeDirectory())/.cargo/bin/uv",
|
||||
"/Library/Frameworks/Python.framework/Versions/3.14/bin/uv",
|
||||
]
|
||||
for c in candidates where FileManager.default.isExecutableFile(atPath: c) {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func which(_ name: String) -> String? {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
||||
process.arguments = ["which", name]
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = Pipe()
|
||||
do { try process.run() } catch { return nil }
|
||||
process.waitUntilExit()
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let path = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return (path?.isEmpty ?? true) ? nil : path
|
||||
}
|
||||
// MARK: - Streaming JSON-lines
|
||||
|
||||
/// Runs a command and collects every parsed JSON document on stdout.
|
||||
/// `onProgress` is called for each document (used for download progress).
|
||||
static func run(command: String, arguments: [String: Any] = [:],
|
||||
onLine: @escaping ([String: Any]) -> Void,
|
||||
completion: @escaping (Int, String?) -> Void) {
|
||||
var cmd = pythonCommand
|
||||
cmd.append(command)
|
||||
if !arguments.isEmpty, let data = try? JSONSerialization.data(withJSONObject: arguments),
|
||||
let str = String(data: data, encoding: .utf8) {
|
||||
cmd.append(str)
|
||||
}
|
||||
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
||||
process.arguments = cmd
|
||||
process.currentDirectoryURL = workingDirectory
|
||||
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
let errPipe = Pipe()
|
||||
process.standardError = errPipe
|
||||
|
||||
var stderrBuffer = ""
|
||||
var stdoutBuffer = ""
|
||||
let outputLock = NSLock()
|
||||
errPipe.fileHandleForReading.readabilityHandler = { handle in
|
||||
let data = handle.availableData
|
||||
if let s = String(data: data, encoding: .utf8) { stderrBuffer += s }
|
||||
}
|
||||
|
||||
pipe.fileHandleForReading.readabilityHandler = { handle in
|
||||
let data = handle.availableData
|
||||
guard !data.isEmpty, let s = String(data: data, encoding: .utf8) else { return }
|
||||
outputLock.lock()
|
||||
stdoutBuffer += s
|
||||
let parts = stdoutBuffer.split(separator: "\n", omittingEmptySubsequences: false)
|
||||
stdoutBuffer = String(parts.last ?? "")
|
||||
let lines = parts.dropLast()
|
||||
outputLock.unlock()
|
||||
for line in lines {
|
||||
if let obj = try? JSONSerialization.jsonObject(with: Data(line.utf8)) as? [String: Any] {
|
||||
onLine(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.terminationHandler = { p in
|
||||
pipe.fileHandleForReading.readabilityHandler = nil
|
||||
errPipe.fileHandleForReading.readabilityHandler = nil
|
||||
outputLock.lock()
|
||||
let finalLine = stdoutBuffer.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
stdoutBuffer = ""
|
||||
outputLock.unlock()
|
||||
if !finalLine.isEmpty,
|
||||
let obj = try? JSONSerialization.jsonObject(with: Data(finalLine.utf8)) as? [String: Any] {
|
||||
onLine(obj)
|
||||
}
|
||||
let error = p.terminationStatus == 0 ? nil : (stderrBuffer.isEmpty ? "O comando \(command) falhou (código \(p.terminationStatus))." : stderrBuffer)
|
||||
completion(Int(p.terminationStatus), error)
|
||||
}
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
} catch {
|
||||
completion(1, error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience: single JSON result
|
||||
|
||||
/// Runs a command and delivers the first parsed JSON document as the result.
|
||||
static func call(command: String, arguments: [String: Any] = [:],
|
||||
completion: @escaping ([String: Any]?, String?) -> Void) {
|
||||
var result: [String: Any]?
|
||||
run(command: command, arguments: arguments, onLine: { obj in
|
||||
if result == nil { result = obj }
|
||||
}, completion: { code, err in
|
||||
completion(result, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user