aboutsummaryrefslogtreecommitdiff
path: root/src_py
diff options
context:
space:
mode:
Diffstat (limited to 'src_py')
-rw-r--r--src_py/opcut/__main__.py1
-rw-r--r--src_py/opcut/common.py198
-rw-r--r--src_py/opcut/csp.py45
-rw-r--r--src_py/opcut/main.py254
-rw-r--r--src_py/opcut/output.py34
-rw-r--r--src_py/opcut/server.py132
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)