diff options
Diffstat (limited to 'src_py')
| -rw-r--r-- | src_py/opcut/__main__.py | 1 | ||||
| -rw-r--r-- | src_py/opcut/common.py | 198 | ||||
| -rw-r--r-- | src_py/opcut/csp.py | 45 | ||||
| -rw-r--r-- | src_py/opcut/main.py | 254 | ||||
| -rw-r--r-- | src_py/opcut/output.py | 34 | ||||
| -rw-r--r-- | src_py/opcut/server.py | 132 |
6 files changed, 312 insertions, 352 deletions
diff --git a/src_py/opcut/__main__.py b/src_py/opcut/__main__.py index 549881a..22ba2f4 100644 --- a/src_py/opcut/__main__.py +++ b/src_py/opcut/__main__.py @@ -4,4 +4,5 @@ from opcut.main import main if __name__ == '__main__': + sys.argv[0] = 'opcut' sys.exit(main()) diff --git a/src_py/opcut/common.py b/src_py/opcut/common.py index d2a56b1..90a8b00 100644 --- a/src_py/opcut/common.py +++ b/src_py/opcut/common.py @@ -1,83 +1,85 @@ +from pathlib import Path import enum +import typing -from hat import util - - -mm = 72 / 25.4 - -Params = util.namedtuple( - 'Params', - ['cut_width', 'float'], - ['panels', 'List[Panel]'], - ['items', 'List[Item]']) - -Result = util.namedtuple( - 'Result', - ['params', 'Params'], - ['used', 'List[Used]'], - ['unused', 'List[Unused]']) - -Panel = util.namedtuple( - 'Panel', - ['id', 'str'], - ['width', 'float'], - ['height', 'float']) - -Item = util.namedtuple( - 'Item', - ['id', 'str'], - ['width', 'float'], - ['height', 'float'], - ['can_rotate', 'bool']) - -Used = util.namedtuple( - 'Used', - ['panel', 'Panel'], - ['item', 'Item'], - ['x', 'float'], - ['y', 'float'], - ['rotate', 'bool']) - -Unused = util.namedtuple( - 'Unused', - ['panel', 'Panel'], - ['width', 'float'], - ['height', 'float'], - ['x', 'float'], - ['y', 'float']) - -OutputSettings = util.namedtuple( - 'OutputSettings', - ['pagesize', 'Tuple[float,float]', (210 * mm, 297 * mm)], - ['margin_top', 'float', 10 * mm], - ['margin_bottom', 'float', 20 * mm], - ['margin_left', 'float', 10 * mm], - ['margin_right', 'float', 10 * mm]) - -Method = enum.Enum('Method', [ - 'GREEDY', - 'FORWARD_GREEDY']) - -OutputType = enum.Enum('OutputType', [ - 'PDF', - 'SVG']) +from hat import json -class UnresolvableError(Exception): - """Exception raised when Result is not solvable""" +mm: float = 72 / 25.4 +package_path: Path = Path(__file__).parent -def params_to_json_data(params): - """Convert params to json serializable data specified by - ``opcut://params.yaml#`` +json_schema_repo: json.SchemaRepository = json.SchemaRepository( + json.json_schema_repo, + json.SchemaRepository.from_json(package_path / 'json_schema_repo.json')) + + +class Panel(typing.NamedTuple): + id: str + width: float + height: float + + +class Item(typing.NamedTuple): + id: str + width: float + height: float + can_rotate: bool + + +class Params(typing.NamedTuple): + cut_width: float + panels: typing.List[Panel] + items: typing.List[Item] + + +class Used(typing.NamedTuple): + panel: Panel + item: Item + x: float + y: float + rotate: bool + + +class Unused(typing.NamedTuple): + panel: Panel + width: float + height: float + x: float + y: float + + +class Result(typing.NamedTuple): + params: Params + used: typing.List[Used] + unused: typing.List[Unused] + + +class OutputSettings(typing.NamedTuple): + pagesize: typing.Tuple[float, float] = (210 * mm, 297 * mm) + margin_top: float = 10 * mm + margin_bottom: float = 20 * mm + margin_left: float = 10 * mm + margin_right: float = 10 * mm - Args: - params (Params): params - Returns: - Any: json serializble data +class Method(enum.Enum): + GREEDY = 'greedy' + FORWARD_GREEDY = 'forward_greedy' - """ + +class OutputType(enum.Enum): + PDF = 'pdf' + SVG = 'svg' + + +class UnresolvableError(Exception): + """Exception raised when Result is not solvable""" + + +def params_to_json(params: Params) -> json.Data: + """Convert params to json serializable data specified by + ``opcut://opcut.yaml#/definitions/params``""" return {'cut_width': params.cut_width, 'panels': {panel.id: {'width': panel.width, 'height': panel.height} @@ -88,41 +90,25 @@ def params_to_json_data(params): for item in params.items}} -def json_data_to_params(json_data): - """Convert json serializable data specified by ``opcut://params.yaml#`` - to params - - Args: - json_data (Any): json serializable data - - Returns: - Params - - """ - return Params(cut_width=json_data['cut_width'], +def params_from_json(data: json.Data) -> Params: + """Convert json serializable data specified by + ``opcut://opcut.yaml#/definitions/params`` to params""" + return Params(cut_width=data['cut_width'], panels=[Panel(id=k, width=v['width'], height=v['height']) - for k, v in json_data['panels'].items()], + for k, v in data['panels'].items()], items=[Item(id=k, width=v['width'], height=v['height'], can_rotate=v['can_rotate']) - for k, v in json_data['items'].items()]) + for k, v in data['items'].items()]) -def result_to_json_data(result): +def result_to_json(result: Result) -> json.Data: """Convert result to json serializable data specified by - ``opcut://result.yaml#`` - - Args: - result (Result): result - - Returns: - Any: json serializble data - - """ - return {'params': params_to_json_data(result.params), + ``opcut://opcut.yaml#/definitions/result``""" + return {'params': params_to_json(result.params), 'used': [{'panel': used.panel.id, 'item': used.item.id, 'x': used.x, @@ -137,18 +123,10 @@ def result_to_json_data(result): for unused in result.unused]} -def json_data_to_result(json_data): - """Convert json serializable data specified by ``opcut://result.yaml#`` - to result - - Args: - json_data (Any): json serializable data - - Returns: - Result - - """ - params = json_data_to_params(json_data['params']) +def result_from_json(data: json.Data) -> Result: + """Convert json serializable data specified by + ``opcut://opcut.yaml#/definitions/result`` to result""" + params = params_from_json(data['params']) panels = {panel.id: panel for panel in params.panels} items = {item.id: item for item in params.items} return Result(params=params, @@ -157,10 +135,10 @@ def json_data_to_result(json_data): x=used['x'], y=used['y'], rotate=used['rotate']) - for used in json_data['used']], + for used in data['used']], unused=[Unused(panel=panels[unused['panel']], width=unused['width'], height=unused['height'], x=unused['x'], y=unused['y']) - for unused in json_data['unused']]) + for unused in data['unused']]) diff --git a/src_py/opcut/csp.py b/src_py/opcut/csp.py index b0e1387..43b8e1f 100644 --- a/src_py/opcut/csp.py +++ b/src_py/opcut/csp.py @@ -3,30 +3,27 @@ import itertools from opcut import common -def calculate(params, method): - """Calculate cutting stock problem - - Args: - params (common.Params): input parameters - method (common.Method): calculation method - - Returns: - Result - - """ - result = common.Result( - params=params, - used=[], - unused=[common.Unused(panel=panel, - width=panel.width, - height=panel.height, - x=0, - y=0) - for panel in params.panels]) - return { - common.Method.GREEDY: _calculate_greedy, - common.Method.FORWARD_GREEDY: _calculate_forward_greedy - }[method](result) +def calculate(params: common.Params, + method: common.Method + ) -> common.Result: + """Calculate cutting stock problem""" + unused = [common.Unused(panel=panel, + width=panel.width, + height=panel.height, + x=0, + y=0) + for panel in params.panels] + result = common.Result(params=params, + used=[], + unused=unused) + + if method == common.Method.GREEDY: + return _calculate_greedy(result) + + elif method == common.Method.FORWARD_GREEDY: + return _calculate_forward_greedy(result) + + raise ValueError('unsupported method') _fitness_K = 0.03 diff --git a/src_py/opcut/main.py b/src_py/opcut/main.py index 02ce856..b428950 100644 --- a/src_py/opcut/main.py +++ b/src_py/opcut/main.py @@ -1,13 +1,13 @@ from pathlib import Path -import argparse import asyncio import contextlib import logging.config import sys +import typing -from hat import util -from hat.util import aio -from hat.util import json +from hat import aio +from hat import json +import click from opcut import common import opcut.csp @@ -15,154 +15,122 @@ import opcut.output import opcut.server -package_path = Path(__file__).parent +log_schema_id: str = 'hat-json://logging.yaml#' +params_schema_id: str = 'opcut://opcut.yaml#/definitions/params' +result_schema_id: str = 'opcut://opcut.yaml#/definitions/result' -default_ui_path = package_path / 'ui' -default_schemas_json_path = package_path / 'schemas_json' - -def main(): +@click.group() +@click.option('--log', + default=None, + metavar='PATH', + type=Path, + help=f"logging configuration file path {log_schema_id}") +def main(log: typing.Optional[Path]): """Application main entry point""" - args = _create_parser().parse_args() - - json_schema_repo = json.SchemaRepository(json.default_schemas_json_path, - args.schemas_json_path) - - if args.log_conf_path: - log_conf = json.decode_file(args.log_conf_path) - json_schema_repo.validate('hat://logging.yaml#', log_conf) - logging.config.dictConfig(log_conf) - - if args.action == 'calculate': - calculate(json_schema_repo, args.params_path, args.method, - args.result_path, args.output_path, args.output_type, - args.output_panel_id, args.output_path) + if not log: + return + + log_conf = json.decode_file(log) + common.json_schema_repo.validate(log_schema_id, log_conf) + logging.config.dictConfig(log_conf) + + +@main.command() +@click.option('--method', + default=common.Method.FORWARD_GREEDY, + type=common.Method, + help="calculate method") +@click.option('--params', + default=None, + metavar='PATH', + type=Path, + help=f"calculate parameters file path ({params_schema_id})") +@click.option('--result', + default=None, + metavar='PATH', + type=Path, + help=f"result file path ({result_schema_id})") +def calculate(method: common.Method, + params: typing.Optional[Path], + result: typing.Optional[Path]): + """Calculate result""" + params = (json.decode_file(params) if params + else json.decode_stream(sys.stdin)) + common.json_schema_repo.validate(params_schema_id, params) + params = common.params_from_json(params) + + res = opcut.csp.calculate(params, method) + res = common.result_to_json(res) + + if result: + json.encode_file(res, result) + else: + json.encode_stream(res, sys.stdout) + + +@main.command() +@click.option('--output-type', + default=common.OutputType.PDF, + type=common.OutputType, + help="output type") +@click.option('--panel', + default=None, + help="panel identifier") +@click.option('--result', + default=None, + metavar='PATH', + type=Path, + help=f"result file path ({result_schema_id})") +@click.option('--output', + default=None, + metavar='PATH', + type=Path, + help="result file path") +def generate_output(output_type: common.OutputType, + panel: typing.Optional[str], + result: typing.Optional[Path], + output: typing.Optional[Path]): + """Generate output""" + result = (json.decode_file(result) if result + else json.decode_stream(sys.stdin)) + common.json_schema_repo.validate(result_schema_id, result) + result = common.result_from_json(result) + + out = opcut.output.generate_output(result, output_type, panel) + + if output: + out.write_bytes(out) + else: + sys.stdout.detach().write(out) + + +@main.command() +@click.option('--host', + default='0.0.0.0', + help="listening host name") +@click.option('--port', + default=8080, + type=int, + help="listening TCP port") +def server(host: str, + port: int): + """Run server""" + aio.init_asyncio() - elif args.action == 'output': - output(json_schema_repo, args.output_path, args.result_path, - args.output_type, args.output_panel_id, args.output_path) + async def run(): + server = await opcut.server.create(host, port) - else: - server(json_schema_repo, args.addr, args.pem_path, args.ui_path) + try: + await server.wait_closing() + finally: + await aio.uncancellable(server.async_close()) -def server(json_schema_repo, addr, ui_pem_path, ui_path): - """Server main entry point""" - aio.init_asyncio() with contextlib.suppress(asyncio.CancelledError): - aio.run_asyncio( - opcut.server.run(json_schema_repo, addr, ui_pem_path, ui_path)) - - -def calculate(json_schema_repo, params_path, method, result_path, output_path, - output_type, output_panel_id): - """Calculate result and generate outputs""" - params_json_data = json.decode_file(params_path) - opcut.json_validator.validate('opcut://params.yaml#', params_json_data) - params = common.json_data_to_params(params_json_data) - result = opcut.csp.calculate(params, method) - result_json_data = common.result_to_json_data(result) - json.encode_file(result_json_data, result_path) - if output_path: - output(json_schema_repo, output_path, result_path, output_type, - output_panel_id) - - -def output(json_schema_repo, output_path, result_path, output_type, - output_panel_id): - """Generate outputs based on calculation result""" - result_json_data = json.decode_file(result_path) - json_schema_repo.validate('opcut://result.yaml#', result_json_data) - result = common.json_data_to_result(result_json_data) - output_bytes = opcut.output.generate_output(result, output_type, - output_panel_id) - with open(output_path, 'wb') as f: - f.write(output_bytes) - - -def _create_parser(): - parser = argparse.ArgumentParser(prog='opcut') - parser.add_argument( - '--log', default=None, metavar='path', dest='log_conf_path', - action=util.EnvPathArgParseAction, - help="logging configuration") - subparsers = parser.add_subparsers(title='actions', dest='action') - - server = subparsers.add_parser('server', help='run web server') - server.add_argument( - '--addr', default='http://0.0.0.0:8080', metavar='addr', dest='addr', - help="address of listening web ui socket formated as " - "'<type>://<host>:<port>' - <type> is 'http' or 'https'; " - "<host> is hostname; <port> is tcp port number " - "(default http://0.0.0.0:8080)") - server.add_argument( - '--pem', default=None, metavar='path', dest='pem_path', - action=util.EnvPathArgParseAction, - help="web front-end pem file path - required for https") - - calculate = subparsers.add_parser('calculate', help='calculate result') - calculate.add_argument( - '--params', required=True, metavar='path', dest='params_path', - action=util.EnvPathArgParseAction, - help="calculate parameters file path " - "(specified by opcut://params.yaml#)") - calculate.add_argument( - '--method', dest='method', - default=common.Method.FORWARD_GREEDY, - choices=[method.name for method in common.Method], - action=_MethodParseAction, - help="calculate method (default FORWARD_GREEDY)") - calculate.add_argument( - '--output', default=None, metavar='path', dest='output_path', - action=util.EnvPathArgParseAction, - help="optional output file path") - - output = subparsers.add_parser('output', help='generate output') - output.add_argument( - '--output', required=True, metavar='path', dest='output_path', - action=util.EnvPathArgParseAction, - help="output file path") - - for p in [calculate, output]: - p.add_argument( - '--result', required=True, metavar='path', dest='result_path', - action=util.EnvPathArgParseAction, - help="calculate result file path " - "(specified by opcut://result.yaml#)") - p.add_argument( - '--output-type', dest='output_type', default='PDF', - choices=[output_type.name for output_type in common.OutputType], - help="output type (default PDF)") - p.add_argument( - '--output-panel', default=None, metavar='panel_id', - dest='output_panel_id', help="output panel id") - - dev_args = parser.add_argument_group('development arguments') - dev_args.add_argument( - '--json-schemas-path', metavar='path', dest='schemas_json_path', - default=default_schemas_json_path, - action=util.EnvPathArgParseAction, - help="override json schemas directory path") - dev_args.add_argument( - '--ui-path', metavar='path', dest='ui_path', - default=default_ui_path, - action=util.EnvPathArgParseAction, - help="override web ui directory path") - - return parser - - -class _MethodParseAction(argparse.Action): - - def __call__(self, parser, namespace, values, option_string=None): - ret = [] - for value in (values if self.nargs else [values]): - try: - ret.append(common.Method[value]) - except Exception as e: - parser.error(str(e)) - setattr(namespace, self.dest, ret if self.nargs else ret[0]) + aio.run_asyncio(run()) if __name__ == '__main__': + sys.argv[0] = 'opcut' sys.exit(main()) diff --git a/src_py/opcut/output.py b/src_py/opcut/output.py index e070710..65f8096 100644 --- a/src_py/opcut/output.py +++ b/src_py/opcut/output.py @@ -1,34 +1,38 @@ import io +import typing import cairo from opcut import common -def generate_output(result, output_type, panel_id=None, - settings=common.OutputSettings()): - """Generate output +def generate_output(result: common.Result, + output_type: common.OutputType, + panel_id: typing.Optional[str] = None, + settings: common.OutputSettings = common.OutputSettings() + ) -> bytes: + """Generate output""" + ret = io.BytesIO() - Args: - result (common.Result): result - output_type (common.OutputType): output type - panel_id (Optional[str]): panel id (None represents all panels) - settings (common.OutputSettings): output settings + if output_type == common.OutputType.PDF: + surface_cls = cairo.PDFSurface - Returns: - bytes + elif output_type == common.OutputType.SVG: + surface_cls = cairo.SVGSurface - """ - ret = io.BytesIO() - surface_cls = {common.OutputType.PDF: cairo.PDFSurface, - common.OutputType.SVG: cairo.SVGSurface}[output_type] - with surface_cls(ret, settings.pagesize[0], + else: + raise ValueError('unsupported output type') + + with surface_cls(ret, + settings.pagesize[0], settings.pagesize[1]) as surface: for panel in result.params.panels: if panel_id and panel.id != panel_id: continue + _write_panel(surface, settings, result, panel) surface.show_page() + return ret.getvalue() diff --git a/src_py/opcut/server.py b/src_py/opcut/server.py index d4b169d..6f9755f 100644 --- a/src_py/opcut/server.py +++ b/src_py/opcut/server.py @@ -1,10 +1,6 @@ -import asyncio -import base64 -import functools -import ssl -import urllib.parse +from pathlib import Path -from hat.util import aio +from hat import aio import aiohttp.web from opcut import common @@ -12,67 +8,83 @@ import opcut.csp import opcut.output -async def run(json_schema_repo, addr, pem_path, ui_path): +static_dir: Path = common.package_path / 'ui' - executor = aio.create_executor() - addr = urllib.parse.urlparse(addr) - if addr.scheme == 'https': - ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) - ssl_ctx.load_cert_chain(pem_path) - else: - ssl_ctx = None - - async def root_handler(request): - raise aiohttp.web.HTTPFound('/index.html') +async def create(host: str, + port: int + ) -> 'Server': + server = Server() + server._async_group = aio.Group() + server._executor = aio.create_executor() app = aiohttp.web.Application() - app.add_routes([aiohttp.web.get('/', root_handler), - aiohttp.web.post('/calculate', functools.partial( - _calculate_handler, json_schema_repo, executor)), - aiohttp.web.post('/generate_output', functools.partial( - _generate_output_handler, json_schema_repo, executor)), - aiohttp.web.static('/', ui_path)]) + app.add_routes([ + aiohttp.web.get('/', server._root_handler), + aiohttp.web.post('/calculate', server._calculate_handler), + aiohttp.web.post('/generate_output', server._generate_output_handler), + aiohttp.web.static('/', static_dir)]) runner = aiohttp.web.AppRunner(app) + await runner.setup() + server.async_group.spawn(aio.call_on_cancel, runner.cleanup) + try: - await runner.setup() - site = aiohttp.web.TCPSite(runner, - host=addr.hostname, - port=addr.port, - ssl_context=ssl_ctx, - shutdown_timeout=0.1) + site = aiohttp.web.TCPSite(runner=runner, + host=host, + port=port, + shutdown_timeout=0.1, + reuse_address=True) await site.start() - await asyncio.Future() - finally: - await runner.cleanup() + except BaseException: + await aio.uncancellable(server.async_close()) + raise -async def _calculate_handler(json_schema_repo, executor, request): - try: - msg = await request.json() - json_schema_repo.validate( - 'opcut://messages.yaml#/definitions/calculate/request', msg) - params = common.json_data_to_params(msg['params']) - method = common.Method[msg['method']] - result = await executor(opcut.csp.calculate, params, method) - result_json_data = common.result_to_json_data(result) - except Exception: - result_json_data = None - return aiohttp.web.json_response({'result': result_json_data}) - - -async def _generate_output_handler(json_schema_repo, executor, request): - try: - msg = await request.json() - json_schema_repo.validate( - 'opcut://messages.yaml#/definitions/generate_output/request', msg) - result = common.json_data_to_result(msg['result']) - output_type = common.OutputType[msg['output_type']] - panel = msg['panel'] - output = await executor(opcut.output.generate_output, result, - output_type, panel) - output_json_data = base64.b64encode(output).decode('utf-8') - except Exception: - output_json_data = None - return aiohttp.web.json_response({'data': output_json_data}) + return server + + +class Server(aio.Resource): + + @property + def async_group(self): + return self._async_group + + async def _root_handler(self, request): + raise aiohttp.web.HTTPFound('/index.html') + + async def _calculate_handler(self, request): + data = await request.json() + common.json_schema_repo.validate( + 'opcut://opcut.yaml#/definitions/params', data) + + method = common.Method(request.query['method']) + params = common.params_from_json(data) + + result = await self._executor(opcut.csp.calculate, params, method) + + return aiohttp.web.json_response(common.result_to_json(result)) + + async def _generate_output_handler(self, request): + data = await request.json() + common.json_schema_repo.validate( + 'opcut://opcut.yaml#/definitions/result', data) + + output_type = common.OutputType(request.query['output_type']) + panel = request.query.get('panel') + result = common.result_from_json(data) + + output = await self._executor(opcut.output.generate_output, result, + output_type, panel) + + if output_type == common.OutputType.PDF: + content_type = 'application/pdf' + + elif output_type == common.OutputType.SVG: + content_type = 'image/svg+xml' + + else: + raise Exception('unsupported output type') + + return aiohttp.web.Response(body=output, + content_type=content_type) |
