-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
backend: extract in grasp_backend package
also keeps backwards compatibility
- Loading branch information
Showing
12 changed files
with
275 additions
and
207 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# this is a hack to monkey patch pytest so it handles tests inside namespace packages without __init__.py properly | ||
# without it, pytest can't discover the package root for some reason | ||
# also see https://github.com/karlicoss/pytest_namespace_pkgs for more | ||
|
||
import pathlib | ||
from typing import Optional | ||
|
||
import _pytest.main | ||
import _pytest.pathlib | ||
|
||
# we consider all dirs in repo/ to be namespace packages | ||
root_dir = pathlib.Path(__file__).absolute().parent.resolve() / 'src' | ||
assert root_dir.exists(), root_dir | ||
|
||
# TODO assert it contains package name?? maybe get it via setuptools.. | ||
|
||
namespace_pkg_dirs = [str(d) for d in root_dir.iterdir() if d.is_dir()] | ||
|
||
# resolve_package_path is called from _pytest.pathlib.import_path | ||
# takes a full abs path to the test file and needs to return the path to the 'root' package on the filesystem | ||
resolve_pkg_path_orig = _pytest.pathlib.resolve_package_path | ||
def resolve_package_path(path: pathlib.Path) -> Optional[pathlib.Path]: | ||
result = path # search from the test file upwards | ||
for parent in result.parents: | ||
if str(parent) in namespace_pkg_dirs: | ||
return parent | ||
raise RuntimeError("Couldn't determine path for ", path) | ||
_pytest.pathlib.resolve_package_path = resolve_package_path | ||
|
||
|
||
# without patching, the orig function returns just a package name for some reason | ||
# (I think it's used as a sort of fallback) | ||
# so we need to point it at the absolute path properly | ||
# not sure what are the consequences.. maybe it wouldn't be able to run against installed packages? not sure.. | ||
search_pypath_orig = _pytest.main.search_pypath | ||
def search_pypath(module_name: str) -> str: | ||
return str(root_dir) | ||
_pytest.main.search_pypath = search_pypath |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,164 +1,16 @@ | ||
#!/usr/bin/env python3 | ||
import argparse | ||
from http.server import BaseHTTPRequestHandler, HTTPServer | ||
import json | ||
import os | ||
import logging | ||
from pathlib import Path | ||
import re | ||
from typing import List, Optional, Dict, Any | ||
|
||
from org_tools import as_org, empty, DEFAULT_TEMPLATE, Config | ||
|
||
CAPTURE_PATH_VAR = 'GRASP_CAPTURE_PATH' | ||
CAPTURE_TEMPLATE_VAR = 'GRASP_CAPTURE_TEMPLATE' | ||
CAPTURE_CONFIG_VAR = 'GRASP_CAPTURE_CONFIG' | ||
|
||
|
||
def get_logger(): | ||
return logging.getLogger('grasp-server') | ||
|
||
|
||
def append_org( | ||
path: Path, | ||
org: str | ||
): | ||
logger = get_logger() | ||
# TODO perhaps should be an error?... | ||
if not path.exists(): | ||
logger.warning("path %s didn't exist!", path) | ||
# https://stackoverflow.com/a/13232181 | ||
if len(org.encode('utf8')) > 4096: | ||
logger.warning("writing out %s might be non-atomic", org) | ||
with path.open('a') as fo: | ||
fo.write(org) | ||
|
||
|
||
from functools import lru_cache | ||
@lru_cache(1) | ||
def capture_config() -> Optional[Config]: | ||
cvar = os.environ.get(CAPTURE_CONFIG_VAR) | ||
if cvar is None: | ||
return None | ||
|
||
globs: Dict[str, Any] = {} | ||
exec(Path(cvar).read_text(), globs) | ||
ConfigClass = globs['Config'] | ||
return ConfigClass() | ||
|
||
|
||
def capture( | ||
url: str, | ||
title, | ||
selection, | ||
comment, | ||
tag_str, | ||
): | ||
logger = get_logger() | ||
# protect strings against None | ||
def safe(s: Optional[str]) -> str: | ||
if s is None: | ||
return '' | ||
else: | ||
return s | ||
capture_path = Path(os.environ[CAPTURE_PATH_VAR]).expanduser() | ||
org_template = os.environ[CAPTURE_TEMPLATE_VAR] | ||
config = capture_config() | ||
logger.info('capturing %s to %s', (url, title, selection, comment, tag_str), capture_path) | ||
|
||
url = safe(url) | ||
title = safe(title) | ||
selection = safe(selection) | ||
comment = safe(comment) | ||
tag_str = safe(tag_str) | ||
|
||
tags: List[str] = [] | ||
if not empty(tag_str): | ||
tags = re.split(r'[\s,]', tag_str) | ||
tags = [t for t in tags if not empty(t)] # just in case | ||
import warnings | ||
warnings.warn("This way of running grasp is deprecated! Please refer to readme and install it as a pip package") | ||
|
||
org = as_org( | ||
url=url, | ||
title=title, | ||
selection=selection, | ||
comment=comment, | ||
tags=tags, | ||
org_template=org_template, | ||
config=config, | ||
) | ||
append_org( | ||
path=capture_path, | ||
org=org, | ||
) | ||
|
||
response = { | ||
'path': str(capture_path), | ||
'status': 'ok', | ||
} | ||
return json.dumps(response).encode('utf8') | ||
|
||
|
||
class GraspRequestHandler(BaseHTTPRequestHandler): | ||
def handle_POST(self): | ||
logger = get_logger() | ||
|
||
content_length = int(self.headers['Content-Length']) | ||
post_data = self.rfile.read(content_length) | ||
payload = json.loads(post_data.decode('utf8')) | ||
logger.info("incoming request %s", payload) | ||
res = capture(**payload) | ||
self.send_response(200) | ||
self.send_header('Content-Type', 'application/json') | ||
self.end_headers() | ||
self.wfile.write(res) | ||
|
||
def respond_error(self, message: str): | ||
self.send_response(500) | ||
self.send_header('Content-Type', 'text/html') | ||
self.end_headers() | ||
self.wfile.write(message.encode('utf8')) | ||
|
||
def do_POST(self): | ||
logger = get_logger() | ||
try: | ||
self.handle_POST() | ||
except Exception as e: | ||
logger.error("Error during processing") | ||
logger.exception(e) | ||
self.respond_error(message=str(e)) | ||
|
||
|
||
def run(port: str, capture_path: str, template: str, config: Optional[Path]): | ||
logger = get_logger() | ||
logger.info("Using template %s", template) | ||
|
||
# not sure if there is a simpler way to communicate with the server... | ||
os.environ[CAPTURE_PATH_VAR] = capture_path | ||
os.environ[CAPTURE_TEMPLATE_VAR] = template | ||
if config is not None: | ||
os.environ[CAPTURE_CONFIG_VAR] = str(config) | ||
httpd = HTTPServer(('', int(port)), GraspRequestHandler) | ||
logger.info(f"Starting httpd on port {port}") | ||
httpd.serve_forever() | ||
|
||
|
||
def setup_parser(p): | ||
p.add_argument('--port', type=str, default='12212', help='Port for communicating with extension') | ||
p.add_argument('--path', type=str, default='~/capture.org', help='File to capture into') | ||
p.add_argument('--template', type=str, default=DEFAULT_TEMPLATE, help=f""" | ||
{as_org.__doc__} | ||
""") | ||
abspath = lambda p: str(Path(p).absolute()) | ||
p.add_argument('--config', type=abspath, required=False, help='Optional dynamic config') | ||
|
||
|
||
def main(): | ||
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s') | ||
from pathlib import Path | ||
|
||
p = argparse.ArgumentParser('grasp server', formatter_class=lambda prog: argparse.ArgumentDefaultsHelpFormatter(prog, width=100)) | ||
setup_parser(p) | ||
args = p.parse_args() | ||
run(args.port, args.path, args.template, args.config) | ||
SRC_DIR = Path(__file__).absolute().parent.parent / 'src' | ||
assert SRC_DIR.exists(), SRC_DIR | ||
|
||
if __name__ == '__main__': | ||
main() | ||
import os | ||
import sys | ||
os.chdir(SRC_DIR) | ||
os.execvp( | ||
sys.executable, | ||
[sys.executable, '-m', 'grasp_backend', 'serve', *sys.argv[1:]] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# NOTE: without __init__.py/__init__.pyi, mypy behaves weird. | ||
# see https://github.com/python/mypy/issues/8584 and the related discussions | ||
# sometime it's kinda valuable to have namespace package and not have __init__.py though, | ||
|
||
# TLDR: you're better off having dimmy pyi, or alternatively you can use 'mypy -p src' (but that's a bit dirty?) | ||
|
||
# todo not sure how it behaves when installed? |
Oops, something went wrong.