├── .gitignore ├── LICENSE ├── README.md ├── examples ├── chat.py ├── hello_world.py ├── random_similarity.py └── to_upper_case.py ├── pyproject.toml ├── requirements.txt ├── setup.py └── src └── obsidian_lab ├── __init__.py └── app.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | __pycache__ 4 | build 5 | dist 6 | 7 | playground 8 | obsidian_lab.egg-info 9 | venv -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Cristian Vasquez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian lab python server 2 | 3 | Say you have a terrific script in python to: 4 | 5 | - Find similar notes to the current one. 6 | - Translate a text. 7 | - Know what was your mood the last three months, just reading your Obsidian vault. 8 | - Whatever wonder you have under the sleeve :D 9 | 10 | And you want to see if it's helpful in Obsidian. 11 | 12 | Then you can: 13 | 14 | 1. Expose your script with this app 15 | 2. Try it out with the [obsidian lab](https://github.com/cristianvasquez/obsidian-lab) plugin. 16 | 17 | ## To install 18 | 19 | ```sh 20 | pip install obsidian-lab 21 | ``` 22 | 23 | Usage: 24 | 25 | ```sh 26 | obsidian-lab 27 | ``` 28 | 29 | This will run a mini web server that exposes the scripts of the directory specified. 30 | 31 | There are some examples in the ./examples directory, to run do: 32 | 33 | ```sh 34 | obsidian-lab ./examples 35 | ``` 36 | 37 | After starting, you can list all the available scripts: 38 | 39 | > GET: http://127.0.0.1:5000/ 40 | 41 | ```json 42 | { 43 | "scripts": [ 44 | "http://127.0.0.1:5000/scripts/hello_world", 45 | "http://127.0.0.1:5000/scripts/random", 46 | "http://127.0.0.1:5000/scripts/to_upper_case" 47 | ] 48 | } 49 | ``` 50 | 51 | To add new scripts, copy them in the scripts directory. 52 | 53 | ## Build 54 | 55 | Install the dependencies 56 | 57 | ```sh 58 | pip install -r requirements.txt 59 | ``` 60 | 61 | try the app 62 | 63 | ```sh 64 | python ./src/obsidian_lab/app.py ./examples 65 | ``` 66 | 67 | ## Status 68 | 69 | This is a proof of concept. -------------------------------------------------------------------------------- /examples/chat.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Plugin: 5 | def __init__(self, *args, **kwargs): 6 | self.plugin_name = os.path.basename(__file__) 7 | super() 8 | 9 | def execute(self, args): 10 | print('request',self.plugin_name,args) 11 | return { 12 | 'contents': f'Hello, {self.plugin_name} ' 13 | } -------------------------------------------------------------------------------- /examples/hello_world.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | class Plugin: 4 | def __init__(self, *args, **kwargs): 5 | self.plugin_name = os.path.basename(__file__) 6 | super() 7 | 8 | def execute(self, args): 9 | print('request',self.plugin_name,args) 10 | return { 11 | 'contents': f'Hello from {self.plugin_name}' 12 | } -------------------------------------------------------------------------------- /examples/random_similarity.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | 4 | class Plugin: 5 | vault_path = '' 6 | max_results = 20 7 | 8 | def __init__(self, vault_path): 9 | self.vault_path = vault_path 10 | self.plugin_name = os.path.basename(__file__) 11 | super() 12 | 13 | 14 | def execute(self, args): 15 | print('request',self.plugin_name,args) 16 | 17 | vault_path = self.vault_path 18 | items = [] 19 | exclude = {'.obsidian', '.trash', '.git'} 20 | for root, dirs, files in os.walk(vault_path, topdown=True): 21 | dirs[:] = [d for d in dirs if d not in exclude] 22 | for file in files: 23 | if file.endswith('.md') and random.random() > 0.5 and len(items) < self.max_results: 24 | items.append({ 25 | 'path': os.path.join(root, file), 26 | 'name': file[:-3], 27 | 'info': { 28 | 'score': random.random() 29 | } 30 | }) 31 | items.sort(key=lambda x: x['info']['score'], reverse=True) 32 | return { 33 | "contents": items, 34 | } 35 | -------------------------------------------------------------------------------- /examples/to_upper_case.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | class Plugin: 4 | 5 | def __init__(self, *args, **kwargs): 6 | self.plugin_name = os.path.basename(__file__) 7 | super() 8 | 9 | 10 | def execute(self, args): 11 | print('request',self.plugin_name,args) 12 | 13 | if "text" in args: 14 | return { 15 | "contents": args['text'].upper() 16 | } 17 | else: 18 | return { 19 | "contents": '' 20 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.1.3 2 | Flask-Cors==3.0.10 3 | Flask-json-schema==0.0.5 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import pathlib 3 | 4 | 5 | # The text of the README file 6 | readme_txt = (pathlib.Path(__file__).parent / "README.md").read_text() 7 | 8 | setuptools.setup( 9 | name='obsidian_lab', 10 | version='0.2.5', 11 | author='Cristian Vasquez', 12 | description='Obsidian lab app', 13 | long_description = readme_txt, 14 | long_description_content_type="text/markdown", 15 | url='https://github.com/cristianvasquez/obsidian-lab-py', 16 | project_urls={ 17 | "Bug Tracker":"https://github.com/cristianvasquez/obsidian-lab-py/issues", 18 | }, 19 | package_dir={"": "src"}, 20 | packages=setuptools.find_packages(where="src"), 21 | license='MIT', 22 | install_requires= [ 23 | 'Flask', 24 | 'Flask-Cors', 25 | 'Flask-json-schema' 26 | ], 27 | classifiers=[ 28 | 'Development Status :: 1 - Planning', 29 | 'Environment :: Console', 30 | 'Environment :: Plugins', 31 | 'Intended Audience :: Science/Research' 32 | ], 33 | entry_points={ 34 | 'console_scripts': [ 35 | 'obsidian-lab = obsidian_lab:main', 36 | ], 37 | }, 38 | python_requires=">=3.6", 39 | ) -------------------------------------------------------------------------------- /src/obsidian_lab/__init__.py: -------------------------------------------------------------------------------- 1 | from .app import main -------------------------------------------------------------------------------- /src/obsidian_lab/app.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import pkgutil 3 | from pathlib import Path 4 | import argparse 5 | import importlib.util 6 | import os 7 | 8 | from flask import Flask, jsonify, request 9 | from flask_cors import CORS 10 | from flask_json_schema import JsonSchema, JsonValidationError 11 | 12 | # import scripts 13 | 14 | #################################################################################### 15 | # config 16 | #################################################################################### 17 | PORT = 5000 18 | HOST = '127.0.0.1' 19 | 20 | app = Flask(__name__) 21 | #################################################################################### 22 | # Cors 23 | #################################################################################### 24 | # Allows access to fetch at 'http://localhost:5000/' from origin 'app://obsidian.md' 25 | obsidian_origin = "app://obsidian.md" 26 | cors = CORS(app, origins=obsidian_origin) 27 | app.config['CORS_HEADERS'] = 'Content-Type' 28 | 29 | #################################################################################### 30 | # Schema 31 | #################################################################################### 32 | # Input schema example: 33 | # { 34 | # vaultPath: "/home/cvasquez/obsidian/development", 35 | # notePath: "snippets-plugin/Test1.md" 36 | # text: "Some selected text", 37 | # } 38 | validator = JsonSchema(app) 39 | input_schema = { 40 | 'required': ['vaultPath'], 41 | 'properties': { 42 | 'vaultPath': {'type': 'string'}, 43 | 'notePath': {'type': 'string'}, 44 | 'text': {'type': 'string'}, 45 | 'script': {'type': 'string'}, 46 | } 47 | } 48 | 49 | 50 | @app.errorhandler(JsonValidationError) 51 | def validation_error(e): 52 | error = { 53 | 'message': e.message, 54 | 'status': 400, 55 | 'errors': [validation_error.message for validation_error in e.errors] 56 | } 57 | return jsonify(error) 58 | 59 | #################################################################################### 60 | # Routers 61 | #################################################################################### 62 | '''Return a list of all detected plugins''' 63 | 64 | @app.route('/', methods=['GET']) 65 | def root(): 66 | 67 | urls = [] 68 | 69 | for path in Path(scripts_path).glob('*.py'): 70 | module_name = str(path.name)[:-3] 71 | urls.append(f'http://{HOST}:{PORT}/{module_name}') 72 | 73 | return { 74 | 'scripts': urls 75 | } 76 | 77 | 78 | '''Exposes a script present in the scripts folder 79 | 80 | The url corresponds to the path to the file, without the 'py' part. 81 | 82 | For example, if there is a script: 83 | 84 | ./scripts/hello_world.py, 85 | 86 | It will be exposed in without the py part at: 87 | 88 | http://{HOST}:{PORT}/{SCRIPTS_FOLDER}/hello_world 89 | 90 | ''' 91 | 92 | @app.route('/', methods=['POST']) 93 | @validator.validate(input_schema) 94 | def execute_script(plugin): 95 | vault_path = request.json['vaultPath'] if 'vaultPath' in request.json else None 96 | 97 | absolute_path = os.path.join(scripts_path, f'{plugin}.py') 98 | 99 | def exec_spec(spec): 100 | plugin = spec.Plugin(vault_path=vault_path) 101 | return plugin.execute(request.json) 102 | 103 | try: 104 | spec = load_spec(plugin, absolute_path) 105 | return exec_spec(spec) 106 | except Exception as e: 107 | return { 108 | 'message': str(e), 109 | 'status': 500, 110 | 'errors': [str(e)] 111 | } 112 | 113 | 114 | @app.route('/', methods=['GET']) 115 | def get_code(plugin): 116 | 117 | absolute_path = os.path.join(scripts_path, f'{plugin}.py') 118 | 119 | return { 120 | "supportedOperation": [ 121 | { 122 | "@type": "Operation", 123 | "method": "POST", 124 | 125 | } 126 | ], 127 | 'absolute': absolute_path 128 | } 129 | 130 | 131 | # The specs of the plugins, to be instantiated 132 | 133 | def load_spec(module_name, absolute_path): 134 | spec = importlib.util.spec_from_file_location(module_name, absolute_path) 135 | foo = importlib.util.module_from_spec(spec) 136 | spec.loader.exec_module(foo) 137 | return foo 138 | 139 | 140 | def main(): 141 | class bcolors: 142 | HEADER = '\033[95m' 143 | OKBLUE = '\033[94m' 144 | OKCYAN = '\033[96m' 145 | OKGREEN = '\033[92m' 146 | WARNING = '\033[93m' 147 | FAIL = '\033[91m' 148 | ENDC = '\033[0m' 149 | BOLD = '\033[1m' 150 | UNDERLINE = '\033[4m' 151 | 152 | 153 | description = ''' 154 | Starts a server for obsidian-lab. Requires a directory with scripts. 155 | 156 | Example scripts: https://github.com/cristianvasquez/obsidian-lab-py/tree/main/examples 157 | 158 | ''' 159 | 160 | parser = argparse.ArgumentParser(description=description) 161 | parser.add_argument("directory", type=str, help="directory containing the scripts") 162 | parser.add_argument("--host", type=str, help="host", default=HOST) 163 | parser.add_argument("-p", "--port", type=int, help="port", default=PORT) 164 | parser.add_argument("-v", "--verbosity", help="verbose mode") 165 | 166 | args = parser.parse_args() 167 | 168 | global scripts_path 169 | scripts_path = str(Path(args.directory).absolute()) 170 | 171 | print(f'running on {scripts_path}') 172 | 173 | files = Path(scripts_path).glob('*.py') 174 | something_found = False 175 | for path in files: 176 | print(f'{bcolors.OKGREEN}Found: {path.absolute()}{bcolors.ENDC}') 177 | something_found = True 178 | 179 | if not something_found: 180 | print(f'''{bcolors.WARNING}Warning: No scripts found in {scripts_path}.{bcolors.ENDC}''') 181 | 182 | app.run(port=args.port, host=args.host) 183 | 184 | if __name__ == '__main__': 185 | main() --------------------------------------------------------------------------------