├── .github └── workflows │ └── continuous-integration.yml ├── .gitignore ├── .gitkeep ├── JupyterKit ├── JupyterError.swift ├── JupyterManager.swift └── JupyterNotebookServer.swift ├── JupyterTool └── JupyterTool.swift ├── Package.swift ├── README.md └── Tests └── JupyterKitTests └── JupyterManagerTests.swift /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | continuous-integration: 9 | strategy: 10 | matrix: 11 | os: 12 | - ubuntu-latest 13 | - macos-latest 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - name: Install SSH Key 17 | uses: shimataro/ssh-key-action@v2 18 | with: 19 | key: ${{ secrets.SSH_KEY }} 20 | known_hosts: ${{ secrets.SSH_KNOWN_HOSTS }} 21 | - uses: actions/checkout@v1 22 | - uses: actions/setup-python@v5 23 | with: 24 | python-version: '3.x' 25 | - name: Install Jupyter Notebook 26 | run: python3 -m pip install jupyter 27 | - name: Test 28 | run: swift test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Hidden Files 2 | .* 3 | !.gitignore 4 | !.gitkeep 5 | !.github 6 | !.travis.yml 7 | 8 | # Swift 9 | /build/ 10 | /Package.resolved 11 | 12 | # Python 13 | *.pyc 14 | 15 | # Temporary Items 16 | *.tmp 17 | *.tmp.* 18 | 19 | # Virtual Environments 20 | /venv*/ 21 | 22 | # Configuration Override 23 | *.override.* 24 | 25 | # Extra Directories 26 | /Assets/ 27 | /Extra/ 28 | 29 | # Xcode 30 | xcuserdata/ 31 | *.xcscmblueprint 32 | *.xccheckout 33 | -------------------------------------------------------------------------------- /.gitkeep: -------------------------------------------------------------------------------- 1 | 6FA0E8DE-C129-470B-AFF3-6D76595BD1D5 2 | -------------------------------------------------------------------------------- /JupyterKit/JupyterError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JupyterError.swift 3 | // JupyterKit 4 | // 5 | // Created by Pedro José Pereira Vieito on 1/4/18. 6 | // Copyright © 2018 Pedro José Pereira Vieito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum JupyterError: LocalizedError { 12 | case executableNotAvailable(URL) 13 | case jupyterError(String) 14 | case openingNotebookNotSupported 15 | case noOutput 16 | 17 | public var errorDescription: String? { 18 | switch self { 19 | case .executableNotAvailable(let url): 20 | return "Jupyter executable not available at “\(url.path)”." 21 | case .jupyterError(let errorString): 22 | return "Jupyter Error: \(errorString)." 23 | case .openingNotebookNotSupported: 24 | return "Opening Notebook on browser is not supported." 25 | case .noOutput: 26 | return "No output available." 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /JupyterKit/JupyterManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JupyterManager.swift 3 | // JupyterKit 4 | // 5 | // Created by Pedro José Pereira Vieito on 10/12/17. 6 | // Copyright © 2017 Pedro José Pereira Vieito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FoundationKit 11 | import PythonKit 12 | 13 | public class JupyterManager { 14 | private static let jupyterNotebookModuleArguments = ["-m", "notebook"] 15 | 16 | internal static func loadPythonProcess() throws -> Process { 17 | try PythonLibrary.loadLibrary() 18 | let sys = try Python.attemptImport("sys") 19 | guard let executable = sys.checking.executable, let executablePath = String(executable) else { 20 | throw NSError(description: "Python executable not available.") 21 | } 22 | return Process(executableURL: executablePath.pathURL) 23 | } 24 | 25 | private static func runJupyterNotebookAndGetOutput(arguments: [String]) throws -> String { 26 | let pythonProcess = try loadPythonProcess() 27 | pythonProcess.standardError = FileHandle.nullDevice 28 | pythonProcess.arguments = JupyterManager.jupyterNotebookModuleArguments + arguments 29 | return try pythonProcess.runAndGetOutputString() 30 | } 31 | 32 | internal static func launchJupyterNotebook(arguments: [String]) throws { 33 | let pythonProcess = try loadPythonProcess() 34 | pythonProcess.standardOutput = FileHandle.nullDevice 35 | pythonProcess.standardError = FileHandle.nullDevice 36 | pythonProcess.arguments = JupyterManager.jupyterNotebookModuleArguments + arguments 37 | try pythonProcess.run() 38 | } 39 | } 40 | 41 | extension JupyterManager { 42 | /// Launches a new notebook server. 43 | /// 44 | /// - Throws: Error trying launch the notebook server. 45 | public static func launchNotebookServer( 46 | launchBrowser: Bool = false, 47 | ip: String? = nil, 48 | port: Int? = nil, 49 | notebookDirectoryURL: URL? = nil 50 | ) throws { 51 | var arguments: [String] = [] 52 | 53 | if let ip = ip { 54 | arguments.append("--ip=\(ip)") 55 | } 56 | if let port = port { 57 | arguments.append("--port=\(port)") 58 | } 59 | if let notebookDirectoryURL = notebookDirectoryURL { 60 | arguments.append("--notebook-dir=\(notebookDirectoryURL.path)") 61 | } 62 | if !launchBrowser { 63 | arguments.append("--no-browser") 64 | } 65 | 66 | var notebooks = try listNotebookServers() 67 | let initalNotebookServersCount = notebooks.count 68 | try JupyterManager.launchJupyterNotebook(arguments: arguments) 69 | while notebooks.count == initalNotebookServersCount { 70 | notebooks = try listNotebookServers() 71 | } 72 | } 73 | 74 | /// Stops all the notebooks running servers. 75 | /// 76 | /// - Throws: Error listing or stopping the running servers. 77 | public static func stopNotebookServers() throws { 78 | let notebooks = try listNotebookServers() 79 | for notebook in notebooks { 80 | try notebook.stop() 81 | } 82 | } 83 | 84 | /// Lists all running notebook servers. 85 | /// 86 | /// - Returns: List of `JupyterInstance` objects. 87 | /// - Throws: Error trying to list running servers. 88 | public static func listNotebookServers() throws -> [JupyterNotebookServer] { 89 | var json = try JupyterManager.runJupyterNotebookAndGetOutput(arguments: ["list", "--json"]) 90 | json = "[\(json.components(separatedBy: .newlines).joined(separator: ","))]" 91 | guard let jsonData = json.data(using: .utf8) else { 92 | throw CocoaError(.coderInvalidValue) 93 | } 94 | 95 | let decoder = JSONDecoder() 96 | return try decoder.decode([JupyterNotebookServer].self, from: jsonData) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /JupyterKit/JupyterNotebookServer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JupyterInstance.swift 3 | // JupyterKit 4 | // 5 | // Created by Pedro José Pereira Vieito on 10/12/17. 6 | // Copyright © 2017 Pedro José Pereira Vieito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FoundationKit 11 | 12 | public struct JupyterNotebookServer: Codable { 13 | /// URL to the Jupyter Notebook server. 14 | public let url: URL 15 | 16 | /// Port of the Jupyter Notebook server. 17 | public let port: Int 18 | 19 | /// Token of the instance. 20 | public let token: String 21 | 22 | /// Process Identifier of the instance. 23 | public let pid: Int 24 | 25 | public let version: String 26 | public let secure: Bool 27 | public let password: Bool 28 | 29 | let hostname: String 30 | let root_dir: String 31 | 32 | /// Session URL to open in a browser with token parameter. 33 | public var sessionURL: URL? { 34 | guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { 35 | return nil 36 | } 37 | 38 | let queryItem = URLQueryItem(name: "token", value: self.token) 39 | urlComponents.queryItems = [queryItem] 40 | return urlComponents.url 41 | } 42 | 43 | /// Identifier of the instance. 44 | public var identifier: String { 45 | return "\(self.hostname):\(self.port)" 46 | } 47 | 48 | /// Instance working directory. 49 | public var notebookDirectory: URL { 50 | return URL(fileURLWithPath: self.root_dir) 51 | } 52 | } 53 | 54 | extension JupyterNotebookServer { 55 | /// Opens the Jupyter Notebook server instance on a browser. 56 | /// 57 | /// - Throws: Error trying to open the instance. 58 | public func open() throws { 59 | guard let sessionURL = self.sessionURL else { 60 | throw JupyterError.openingNotebookNotSupported 61 | } 62 | 63 | try sessionURL.open() 64 | } 65 | 66 | /// Stops the Jupyter Notebook server instance. 67 | /// 68 | /// - Throws: Error trying to stop the server instance. 69 | public func stop() throws { 70 | let portString = String(port) 71 | try JupyterManager.launchJupyterNotebook(arguments: ["stop", portString]) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /JupyterTool/JupyterTool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JupyterTool.swift 3 | // JupyterTool 4 | // 5 | // Created by Pedro José Pereira Vieito on 30/1/18. 6 | // Copyright © 2018 Pedro José Pereira Vieito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import LoggerKit 11 | import JupyterKit 12 | import ArgumentParser 13 | 14 | @main 15 | struct JupyterTool: ParsableCommand { 16 | static var configuration: CommandConfiguration { 17 | return CommandConfiguration(commandName: String(describing: Self.self)) 18 | } 19 | 20 | @Flag(name: .shortAndLong, help: "Open running instances or launch a new one.") 21 | var `open`: Bool = false 22 | 23 | @Flag(name: .shortAndLong, help: "Lauch a new instance.") 24 | var new: Bool = false 25 | 26 | @Flag(name: .shortAndLong, help: "Stop all running instances.") 27 | var kill: Bool = false 28 | 29 | @Flag(name: .shortAndLong, help: "Verbose mode.") 30 | var verbose: Bool = false 31 | 32 | @Flag(name: .shortAndLong, help: "Debug mode.") 33 | var debug: Bool = false 34 | 35 | func run() throws { 36 | do { 37 | Logger.logMode = .commandLine 38 | Logger.logLevel = self.verbose ? .verbose : .info 39 | Logger.logLevel = self.debug ? .debug : Logger.logLevel 40 | 41 | var notebooks = try JupyterManager.listNotebookServers() 42 | if (notebooks.isEmpty && self.open) || self.new { 43 | try JupyterManager.launchNotebookServer(launchBrowser: false) 44 | } 45 | notebooks = try JupyterManager.listNotebookServers() 46 | 47 | guard !notebooks.isEmpty else { 48 | Logger.log(warning: "No Jupyter Notebook instances running.") 49 | return 50 | } 51 | 52 | Logger.log(important: "Jupyter Notebooks (\(notebooks.count))") 53 | 54 | for notebook in notebooks { 55 | Logger.log(success: "Notebook “\(notebook.identifier)”") 56 | Logger.log(verbose: "Directory: \(notebook.notebookDirectory.path)") 57 | Logger.log(verbose: "URL: \(notebook.url.absoluteURL)") 58 | Logger.log(verbose: "Port: \(notebook.port)") 59 | Logger.log(verbose: "Secure: \(notebook.secure)") 60 | Logger.log(verbose: "Token: \(notebook.token)") 61 | 62 | if let sessionURL = notebook.sessionURL { 63 | Logger.log(debug: "Session URL: \(sessionURL)") 64 | } 65 | 66 | if self.open { 67 | do { 68 | try notebook.open() 69 | } 70 | catch { 71 | Logger.log(warning: error) 72 | } 73 | } 74 | 75 | if self.kill { 76 | do { 77 | Logger.log(notice: "Killing Notebook “\(notebook.identifier)”...") 78 | try notebook.stop() 79 | } 80 | catch { 81 | Logger.log(warning: error) 82 | } 83 | } 84 | } 85 | } 86 | catch { 87 | Logger.log(fatalError: error) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "JupyterTool", 7 | platforms: [ 8 | .macOS(.v10_13) 9 | ], 10 | products: [ 11 | .executable( 12 | name: "JupyterTool", 13 | targets: ["JupyterTool"] 14 | ), 15 | .library( 16 | name: "JupyterKit", 17 | targets: ["JupyterKit"] 18 | ) 19 | ], 20 | dependencies: [ 21 | .package(url: "git@github.com:pvieito/LoggerKit.git", branch: "master"), 22 | .package(url: "git@github.com:pvieito/FoundationKit.git", branch: "master"), 23 | .package(url: "git@github.com:pvieito/PythonKit.git", branch: "master"), 24 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), 25 | ], 26 | targets: [ 27 | .executableTarget( 28 | name: "JupyterTool", 29 | dependencies: [ 30 | "LoggerKit", 31 | "JupyterKit", 32 | .product(name: "ArgumentParser", package: "swift-argument-parser") 33 | ], 34 | path: "JupyterTool" 35 | ), 36 | .target( 37 | name: "JupyterKit", 38 | dependencies: [ 39 | "FoundationKit", 40 | "PythonKit", 41 | ], 42 | path: "JupyterKit" 43 | ), 44 | .testTarget( 45 | name: "JupyterKitTests", 46 | dependencies: ["JupyterKit"] 47 | ) 48 | ] 49 | ) 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JupyterKit 2 | 3 | Swift framework and tool to launch and control Jupyter Notebooks. 4 | 5 | ## Requirements 6 | 7 | `JupyterKit` requires Swift 5.1 or later and has been tested both on macOS and Linux. 8 | 9 | ## JupyterTool 10 | 11 | `JupyterTool` is a command line tool for macOS and Linux that can list the running instances of Jupyter notebooks, inspect their properties and terminate them. It is powered by the `JupyterKit` framework. 12 | -------------------------------------------------------------------------------- /Tests/JupyterKitTests/JupyterManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JupyterManagerTests.swift 3 | // JupyterKitTests 4 | // 5 | // Created by Pedro José Pereira Vieito on 22/11/2019. 6 | // Copyright © 2019 Pedro José Pereira Vieito. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FoundationKit 11 | import XCTest 12 | @testable import JupyterKit 13 | 14 | class JupyterKitTests: XCTestCase { 15 | func testJupyterManager() throws { 16 | do { 17 | try JupyterManager.stopNotebookServers() 18 | try JupyterManager.launchNotebookServer() 19 | let l1 = try JupyterManager.listNotebookServers() 20 | 21 | XCTAssertEqual(l1.count, 1) 22 | 23 | try JupyterManager.launchNotebookServer() 24 | try JupyterManager.launchNotebookServer() 25 | let l2 = try JupyterManager.listNotebookServers() 26 | 27 | XCTAssertEqual(l2.count, 3) 28 | 29 | let l2s = l2.last! 30 | try l2s.stop() 31 | 32 | Thread.sleep(forTimeInterval: 1.0) 33 | 34 | let l3 = try JupyterManager.listNotebookServers() 35 | 36 | let l2Set = Set(l2[0...1].map({ $0.identifier })) 37 | let l3Set = Set(l3.map({ $0.identifier })) 38 | 39 | XCTAssertEqual(l3.count, 2) 40 | XCTAssertEqual(l3Set, l2Set) 41 | XCTAssertTrue(l3Set.intersection(Set([l2s.identifier])).isEmpty) 42 | 43 | try JupyterManager.stopNotebookServers() 44 | 45 | Thread.sleep(forTimeInterval: 1.0) 46 | 47 | let l4 = try JupyterManager.listNotebookServers() 48 | 49 | XCTAssertEqual(l4.count, 0) 50 | return 51 | } 52 | catch { 53 | try? JupyterManager.stopNotebookServers() 54 | throw error 55 | } 56 | } 57 | } 58 | --------------------------------------------------------------------------------