diff options
Diffstat (limited to 'src/services/api')
18 files changed, 478 insertions, 9 deletions
diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py index 84d719fda..049d59de7 100644 --- a/src/services/api/graphql/bindings.py +++ b/src/services/api/graphql/bindings.py @@ -17,11 +17,14 @@ import vyos.defaults from . graphql.queries import query from . graphql.mutations import mutation from . graphql.directives import directives_dict +from . utils.schema_from_op_mode import generate_op_mode_definitions from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers def generate_schema(): api_schema_dir = vyos.defaults.directories['api_schema'] + generate_op_mode_definitions() + type_defs = load_schema_from_path(api_schema_dir) schema = make_executable_schema(type_defs, query, mutation, snake_case_fallback_resolvers, directives=directives_dict) diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py index 0a9298f55..d8ceefae6 100644 --- a/src/services/api/graphql/graphql/directives.py +++ b/src/services/api/graphql/graphql/directives.py @@ -48,6 +48,14 @@ class ShowConfigDirective(VyosDirective): super().visit_field_definition(field, object_type, make_resolver=make_show_config_resolver) +class SystemStatusDirective(VyosDirective): + """ + Class providing implementation of 'system_status' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_system_status_resolver) + class ConfigFileDirective(VyosDirective): """ Class providing implementation of 'configfile' directive in schema. @@ -72,8 +80,27 @@ class ImageDirective(VyosDirective): super().visit_field_definition(field, object_type, make_resolver=make_image_resolver) +class GenOpQueryDirective(VyosDirective): + """ + Class providing implementation of 'genopquery' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_gen_op_query_resolver) + +class GenOpMutationDirective(VyosDirective): + """ + Class providing implementation of 'genopmutation' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_gen_op_mutation_resolver) + directives_dict = {"configure": ConfigureDirective, "showconfig": ShowConfigDirective, + "systemstatus": SystemStatusDirective, "configfile": ConfigFileDirective, "show": ShowDirective, - "image": ImageDirective} + "image": ImageDirective, + "genopquery": GenOpQueryDirective, + "genopmutation": GenOpMutationDirective} diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 0c3eb702a..3e89fb239 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -1,4 +1,4 @@ -# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -20,6 +20,7 @@ from graphql import GraphQLResolveInfo from makefun import with_signature from .. import state +from .. import key_auth from api.graphql.recipes.session import Session mutation = ObjectType("Mutation") @@ -53,6 +54,19 @@ def make_mutation_resolver(mutation_name, class_name, session_func): } data = kwargs['data'] + key = data['key'] + + auth = key_auth.auth_required(key) + if auth is None: + return { + "success": False, + "errors": ['invalid API key'] + } + + # We are finished with the 'key' entry, and may remove so as to + # pass the rest of data (if any) to function. + del data['key'] + session = state.settings['app'].state.vyos_session # one may override the session functions with a local subclass @@ -61,7 +75,7 @@ def make_mutation_resolver(mutation_name, class_name, session_func): klass = getattr(mod, class_name) except ImportError: # otherwise, dynamically generate subclass to invoke subclass - # name based templates + # name based functions klass = type(class_name, (Session,), {}) k = klass(session, data) method = getattr(k, session_func) @@ -97,3 +111,7 @@ def make_config_file_resolver(mutation_name): def make_image_resolver(mutation_name): return make_prefix_resolver(mutation_name, prefix=['add', 'delete']) + +def make_gen_op_mutation_resolver(mutation_name): + class_name = mutation_name + return make_mutation_resolver(mutation_name, class_name, 'gen_op_mutation') diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index e1868091e..f6544709e 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -1,4 +1,4 @@ -# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -20,6 +20,7 @@ from graphql import GraphQLResolveInfo from makefun import with_signature from .. import state +from .. import key_auth from api.graphql.recipes.session import Session query = ObjectType("Query") @@ -53,6 +54,19 @@ def make_query_resolver(query_name, class_name, session_func): } data = kwargs['data'] + key = data['key'] + + auth = key_auth.auth_required(key) + if auth is None: + return { + "success": False, + "errors": ['invalid API key'] + } + + # We are finished with the 'key' entry, and may remove so as to + # pass the rest of data (if any) to function. + del data['key'] + session = state.settings['app'].state.vyos_session # one may override the session functions with a local subclass @@ -61,7 +75,7 @@ def make_query_resolver(query_name, class_name, session_func): klass = getattr(mod, class_name) except ImportError: # otherwise, dynamically generate subclass to invoke subclass - # name based templates + # name based functions klass = type(class_name, (Session,), {}) k = klass(session, data) method = getattr(k, session_func) @@ -84,6 +98,14 @@ def make_show_config_resolver(query_name): class_name = query_name return make_query_resolver(query_name, class_name, 'show_config') +def make_system_status_resolver(query_name): + class_name = query_name + return make_query_resolver(query_name, class_name, 'system_status') + def make_show_resolver(query_name): class_name = query_name return make_query_resolver(query_name, class_name, 'show') + +def make_gen_op_query_resolver(query_name): + class_name = query_name + return make_query_resolver(query_name, class_name, 'gen_op_query') diff --git a/src/services/api/graphql/graphql/schema/config_file.graphql b/src/services/api/graphql/graphql/schema/config_file.graphql index 31ab26b9e..a7263114b 100644 --- a/src/services/api/graphql/graphql/schema/config_file.graphql +++ b/src/services/api/graphql/graphql/schema/config_file.graphql @@ -1,4 +1,5 @@ input SaveConfigFileInput { + key: String! fileName: String } @@ -13,6 +14,7 @@ type SaveConfigFileResult { } input LoadConfigFileInput { + key: String! fileName: String! } diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql index 25f091bfa..345c349ac 100644 --- a/src/services/api/graphql/graphql/schema/dhcp_server.graphql +++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql @@ -1,4 +1,5 @@ input DhcpServerConfigInput { + key: String! sharedNetworkName: String subnet: String defaultRouter: String diff --git a/src/services/api/graphql/graphql/schema/firewall_group.graphql b/src/services/api/graphql/graphql/schema/firewall_group.graphql index d89904b9e..9454d2997 100644 --- a/src/services/api/graphql/graphql/schema/firewall_group.graphql +++ b/src/services/api/graphql/graphql/schema/firewall_group.graphql @@ -1,4 +1,5 @@ input CreateFirewallAddressGroupInput { + key: String! name: String! address: [String] } @@ -15,6 +16,7 @@ type CreateFirewallAddressGroupResult { } input UpdateFirewallAddressGroupMembersInput { + key: String! name: String! address: [String!]! } @@ -31,6 +33,7 @@ type UpdateFirewallAddressGroupMembersResult { } input RemoveFirewallAddressGroupMembersInput { + key: String! name: String! address: [String!]! } @@ -47,6 +50,7 @@ type RemoveFirewallAddressGroupMembersResult { } input CreateFirewallAddressIpv6GroupInput { + key: String! name: String! address: [String] } @@ -63,6 +67,7 @@ type CreateFirewallAddressIpv6GroupResult { } input UpdateFirewallAddressIpv6GroupMembersInput { + key: String! name: String! address: [String!]! } @@ -79,6 +84,7 @@ type UpdateFirewallAddressIpv6GroupMembersResult { } input RemoveFirewallAddressIpv6GroupMembersInput { + key: String! name: String! address: [String!]! } diff --git a/src/services/api/graphql/graphql/schema/image.graphql b/src/services/api/graphql/graphql/schema/image.graphql index 7d1b4f9d0..485033875 100644 --- a/src/services/api/graphql/graphql/schema/image.graphql +++ b/src/services/api/graphql/graphql/schema/image.graphql @@ -1,4 +1,5 @@ input AddSystemImageInput { + key: String! location: String! } @@ -14,6 +15,7 @@ type AddSystemImageResult { } input DeleteSystemImageInput { + key: String! name: String! } diff --git a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql index 32438b315..8a17d919f 100644 --- a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql +++ b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql @@ -1,4 +1,5 @@ input InterfaceEthernetConfigInput { + key: String! interface: String address: String replace: Boolean = true diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql index 952e46f34..624be2620 100644 --- a/src/services/api/graphql/graphql/schema/schema.graphql +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -7,11 +7,17 @@ directive @configure on FIELD_DEFINITION directive @configfile on FIELD_DEFINITION directive @show on FIELD_DEFINITION directive @showconfig on FIELD_DEFINITION +directive @systemstatus on FIELD_DEFINITION directive @image on FIELD_DEFINITION +directive @genopquery on FIELD_DEFINITION +directive @genopmutation on FIELD_DEFINITION + +scalar Generic type Query { Show(data: ShowInput) : ShowResult @show ShowConfig(data: ShowConfigInput) : ShowConfigResult @showconfig + SystemStatus(data: SystemStatusInput) : SystemStatusResult @systemstatus } type Mutation { diff --git a/src/services/api/graphql/graphql/schema/show.graphql b/src/services/api/graphql/graphql/schema/show.graphql index c7709e48b..278ed536b 100644 --- a/src/services/api/graphql/graphql/schema/show.graphql +++ b/src/services/api/graphql/graphql/schema/show.graphql @@ -1,4 +1,5 @@ input ShowInput { + key: String! path: [String!]! } diff --git a/src/services/api/graphql/graphql/schema/show_config.graphql b/src/services/api/graphql/graphql/schema/show_config.graphql index 34afd2aa9..5a1fe43da 100644 --- a/src/services/api/graphql/graphql/schema/show_config.graphql +++ b/src/services/api/graphql/graphql/schema/show_config.graphql @@ -2,9 +2,9 @@ Use 'scalar Generic' for show config output, to avoid attempts to JSON-serialize in case of JSON output. """ -scalar Generic input ShowConfigInput { + key: String! path: [String!]! configFormat: String } diff --git a/src/services/api/graphql/graphql/schema/system_status.graphql b/src/services/api/graphql/graphql/schema/system_status.graphql new file mode 100644 index 000000000..be8d87535 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/system_status.graphql @@ -0,0 +1,18 @@ +""" +Use 'scalar Generic' for system status output, to avoid attempts to +JSON-serialize in case of JSON output. +""" + +input SystemStatusInput { + key: String! +} + +type SystemStatus { + result: Generic +} + +type SystemStatusResult { + data: SystemStatus + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/key_auth.py b/src/services/api/graphql/key_auth.py new file mode 100644 index 000000000..f756ed6d8 --- /dev/null +++ b/src/services/api/graphql/key_auth.py @@ -0,0 +1,18 @@ + +from . import state + +def check_auth(key_list, key): + if not key_list: + return None + key_id = None + for k in key_list: + if k['key'] == key: + key_id = k['id'] + return key_id + +def auth_required(key): + api_keys = None + api_keys = state.settings['app'].state.vyos_keys + key_id = check_auth(api_keys, key) + state.settings['app'].state.vyos_id = key_id + return key_id diff --git a/src/services/api/graphql/recipes/queries/system_status.py b/src/services/api/graphql/recipes/queries/system_status.py new file mode 100755 index 000000000..8dadcc9f3 --- /dev/null +++ b/src/services/api/graphql/recipes/queries/system_status.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import os +import sys +import json +import importlib.util + +from vyos.defaults import directories + +from api.graphql.utils.util import load_op_mode_as_module + +def get_system_version() -> dict: + show_version = load_op_mode_as_module('version.py') + return show_version.show(raw=True, funny=False) + +def get_system_uptime() -> dict: + show_uptime = load_op_mode_as_module('show_uptime.py') + return show_uptime.get_raw_data() + +def get_system_ram_usage() -> dict: + show_ram = load_op_mode_as_module('memory.py') + return show_ram.show(raw=True) diff --git a/src/services/api/graphql/recipes/session.py b/src/services/api/graphql/recipes/session.py index 1f844ff70..ac185beb7 100644 --- a/src/services/api/graphql/recipes/session.py +++ b/src/services/api/graphql/recipes/session.py @@ -1,4 +1,4 @@ -# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -13,15 +13,20 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. +import os import json from ariadne import convert_camel_case_to_snake -import vyos.defaults from vyos.config import Config from vyos.configtree import ConfigTree +from vyos.defaults import directories from vyos.template import render +from api.graphql.utils.util import load_op_mode_as_module, split_compound_op_mode_name + +op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json') + class Session: """ Wrapper for calling configsession functions based on GraphQL requests. @@ -33,6 +38,12 @@ class Session: self._data = data self._name = convert_camel_case_to_snake(type(self).__name__) + try: + with open(op_mode_include_file) as f: + self._op_mode_list = json.loads(f.read()) + except Exception: + self._op_mode_list = None + def configure(self): session = self._session data = self._data @@ -40,7 +51,7 @@ class Session: tmpl_file = f'{func_base_name}.tmpl' cmd_file = f'/tmp/{func_base_name}.cmds' - tmpl_dir = vyos.defaults.directories['api_templates'] + tmpl_dir = directories['api_templates'] try: render(cmd_file, tmpl_file, data, location=tmpl_dir) @@ -136,3 +147,61 @@ class Session: raise error return res + + def system_status(self): + import api.graphql.recipes.queries.system_status as system_status + + session = self._session + data = self._data + + status = {} + status['host_name'] = session.show(['host', 'name']).strip() + status['version'] = system_status.get_system_version() + status['uptime'] = system_status.get_system_uptime() + status['ram'] = system_status.get_system_ram_usage() + + return status + + def gen_op_query(self): + session = self._session + data = self._data + name = self._name + op_mode_list = self._op_mode_list + + # handle the case that the op-mode file contains underscores: + if op_mode_list is None: + raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'") + (func_name, scriptname) = split_compound_op_mode_name(name, op_mode_list) + if scriptname == '': + raise FileNotFoundError(f"No op-mode file named in string '{name}'") + + mod = load_op_mode_as_module(f'{scriptname}') + func = getattr(mod, func_name) + if len(list(data)) > 0: + res = func(True, **data) + else: + res = func(True) + + return res + + def gen_op_mutation(self): + session = self._session + data = self._data + name = self._name + op_mode_list = self._op_mode_list + + # handle the case that the op-mode file name contains underscores: + if op_mode_list is None: + raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'") + (func_name, scriptname) = split_compound_op_mode_name(name, op_mode_list) + if scriptname == '': + raise FileNotFoundError(f"No op-mode file named in string '{name}'") + + mod = load_op_mode_as_module(f'{scriptname}') + func = getattr(mod, func_name) + if len(list(data)) > 0: + res = func(**data) + else: + res = func() + + return res diff --git a/src/services/api/graphql/utils/schema_from_op_mode.py b/src/services/api/graphql/utils/schema_from_op_mode.py new file mode 100755 index 000000000..d27586747 --- /dev/null +++ b/src/services/api/graphql/utils/schema_from_op_mode.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# +# A utility to generate GraphQL schema defintions from standardized op-mode +# scripts. + +import os +import json +import typing +from inspect import signature, getmembers, isfunction +from jinja2 import Template + +from vyos.defaults import directories +from . util import load_as_module, is_op_mode_function_name, is_show_function_name + +OP_MODE_PATH = directories['op_mode'] +SCHEMA_PATH = directories['api_schema'] +DATA_DIR = directories['data'] + +op_mode_include_file = os.path.join(DATA_DIR, 'op-mode-standardized.json') + +schema_data: dict = {'schema_name': '', + 'schema_fields': []} + +query_template = """ +input {{ schema_name }}Input { + key: String! + {%- for field_entry in schema_fields %} + {{ field_entry }} + {%- endfor %} +} + +type {{ schema_name }} { + result: Generic +} + +type {{ schema_name }}Result { + data: {{ schema_name }} + success: Boolean! + errors: [String] +} + +extend type Query { + {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopquery +} +""" + +mutation_template = """ +input {{ schema_name }}Input { + key: String! + {%- for field_entry in schema_fields %} + {{ field_entry }} + {%- endfor %} +} + +type {{ schema_name }} { + result: Generic +} + +type {{ schema_name }}Result { + data: {{ schema_name }} + success: Boolean! + errors: [String] +} + +extend type Mutation { + {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopmutation +} +""" + +def _snake_to_pascal_case(name: str) -> str: + res = ''.join(map(str.title, name.split('_'))) + return res + +def _map_type_name(type_name: type, optional: bool = False) -> str: + if type_name == str: + return 'String!' if not optional else 'String = null' + if type_name == int: + return 'Int!' if not optional else 'Int = null' + if type_name == bool: + return 'Boolean!' if not optional else 'Boolean = false' + if typing.get_origin(type_name) == list: + if not optional: + return f'[{_map_type_name(typing.get_args(type_name)[0])}]!' + return f'[{_map_type_name(typing.get_args(type_name)[0])}]' + # typing.Optional is typing.Union[_, NoneType] + if (typing.get_origin(type_name) is typing.Union and + typing.get_args(type_name)[1] == type(None)): + return f'{_map_type_name(typing.get_args(type_name)[0], optional=True)}' + + # scalar 'Generic' is defined in schema.graphql + return 'Generic' + +def create_schema(func_name: str, base_name: str, func: callable) -> str: + sig = signature(func) + + field_dict = {} + for k in sig.parameters: + field_dict[sig.parameters[k].name] = _map_type_name(sig.parameters[k].annotation) + + # It is assumed that if one is generating a schema for a 'show_*' + # function, that 'get_raw_data' is present and 'raw' is desired. + if 'raw' in list(field_dict): + del field_dict['raw'] + + schema_fields = [] + for k,v in field_dict.items(): + schema_fields.append(k+': '+v) + + schema_data['schema_name'] = _snake_to_pascal_case(func_name + '_' + base_name) + schema_data['schema_fields'] = schema_fields + + if is_show_function_name(func_name): + j2_template = Template(query_template) + else: + j2_template = Template(mutation_template) + + res = j2_template.render(schema_data) + + return res + +def generate_op_mode_definitions(): + with open(op_mode_include_file) as f: + op_mode_files = json.load(f) + + for file in op_mode_files: + basename = os.path.splitext(file)[0].replace('-', '_') + module = load_as_module(basename, os.path.join(OP_MODE_PATH, file)) + + funcs = getmembers(module, isfunction) + funcs = list(filter(lambda ft: is_op_mode_function_name(ft[0]), funcs)) + + funcs_dict = {} + for (name, thunk) in funcs: + funcs_dict[name] = thunk + + results = [] + for name,func in funcs_dict.items(): + res = create_schema(name, basename, func) + results.append(res) + + out = '\n'.join(results) + with open(f'{SCHEMA_PATH}/{basename}.graphql', 'w') as f: + f.write(out) + +if __name__ == '__main__': + generate_op_mode_definitions() diff --git a/src/services/api/graphql/utils/util.py b/src/services/api/graphql/utils/util.py new file mode 100644 index 000000000..073126853 --- /dev/null +++ b/src/services/api/graphql/utils/util.py @@ -0,0 +1,76 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import importlib.util + +from vyos.defaults import directories + +def load_as_module(name: str, path: str): + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + +def load_op_mode_as_module(name: str): + path = os.path.join(directories['op_mode'], name) + name = os.path.splitext(name)[0].replace('-', '_') + return load_as_module(name, path) + +def is_op_mode_function_name(name): + if re.match(r"^(show|clear|reset|restart)", name): + return True + return False + +def is_show_function_name(name): + if re.match(r"^show", name): + return True + return False + +def _nth_split(delim: str, n: int, s: str): + groups = s.split(delim) + l = len(groups) + if n > l-1 or n < 1: + return (s, '') + return (delim.join(groups[:n]), delim.join(groups[n:])) + +def _nth_rsplit(delim: str, n: int, s: str): + groups = s.split(delim) + l = len(groups) + if n > l-1 or n < 1: + return (s, '') + return (delim.join(groups[:l-n]), delim.join(groups[l-n:])) + +# Since we have mangled possible hyphens in the file name while constructing +# the snake case of the query/mutation name, we will need to recover the +# file name by searching with mangling: +def _filter_on_mangled(test): + def func(elem): + mangle = os.path.splitext(elem)[0].replace('-', '_') + return test == mangle + return func + +# Find longest name in concatenated string that matches the basename of an +# op-mode script. Should one prefer to concatenate in the reverse order +# (script_name + '_' + function_name), use _nth_rsplit. +def split_compound_op_mode_name(name: str, files: list): + for i in range(1, name.count('_') + 1): + pair = _nth_split('_', i, name) + f = list(filter(_filter_on_mangled(pair[1]), files)) + if f: + pair = (pair[0], f[0]) + return pair + return (name, '') |