• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

datopian / ckanext-scheming / 10373747183

13 Aug 2024 04:37PM UTC coverage: 81.492% (+1.4%) from 80.089%
10373747183

push

github

steveoni
update resourceupload

841 of 1032 relevant lines covered (81.49%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.07
/ckanext/scheming/plugins.py
1
#!/usr/bin/env python
2
# encoding: utf-8
3
import os
1✔
4
import inspect
1✔
5
import logging
1✔
6
from functools import wraps
1✔
7

8
import six
1✔
9
import yaml
1✔
10
import ckan.plugins as p
1✔
11

12
try:
1✔
13
    from paste.reloader import watch_file
1✔
14
except ImportError:
1✔
15
    watch_file = None
1✔
16

17
import ckan.model as model
1✔
18
from ckan.common import c, json
1✔
19
from ckan.lib.navl.dictization_functions import unflatten, flatten_schema
1✔
20
try:
1✔
21
    from ckan.lib.helpers import helper_functions as core_helper_functions
1✔
22
except ImportError:  # CKAN <= 2.5
×
23
    core_helper_functions = None
×
24

25
from ckantoolkit import (
1✔
26
    DefaultDatasetForm,
27
    DefaultGroupForm,
28
    DefaultOrganizationForm,
29
    get_validator,
30
    get_converter,
31
    navl_validate,
32
    add_template_directory,
33
    add_resource,
34
    add_public_directory,
35
    missing,
36
    check_ckan_version,
37
)
38

39
from ckanext.scheming import helpers, validation, logic, loader, views
1✔
40
from ckanext.scheming.errors import SchemingException
1✔
41

42
ignore_missing = get_validator('ignore_missing')
1✔
43
not_empty = get_validator('not_empty')
1✔
44
convert_to_extras = get_converter('convert_to_extras')
1✔
45
convert_from_extras = get_converter('convert_from_extras')
1✔
46

47
DEFAULT_PRESETS = 'ckanext.scheming:presets.json'
1✔
48

49
log = logging.getLogger(__name__)
1✔
50

51
def run_once_for_caller(var_name, rval_fn):
1✔
52
    """
53
    return passed value if this method has been called more than once
54
    from the same function, e.g. load_plugin_helpers, get_validator
55

56
    This lets us have multiple scheming plugins active without repeating
57
    helpers, validators, template dirs and to be compatible with versions
58
    of ckan that don't support overwriting helpers/validators
59
    """
60
    import inspect
1✔
61

62
    def decorator(fn):
1✔
63
        @wraps(fn)
1✔
64
        def wrapper(*args, **kwargs):
1✔
65
            caller = inspect.currentframe().f_back
1✔
66
            if var_name in caller.f_locals:
1✔
67
                return rval_fn()
1✔
68
            # inject local varible into caller to track separate calls (reloading)
69
            caller.f_locals[var_name] = None
1✔
70
            return fn(*args, **kwargs)
1✔
71
        return wrapper
1✔
72
    return decorator
1✔
73

74

75
class _SchemingMixin(object):
1✔
76
    """
77
    Store single plugin instances in class variable 'instance'
78

79
    All plugins below need helpers and template directories, but we should
80
    only do them once when any plugin is loaded.
81
    """
82
    instance = None
1✔
83
    _presets = None
1✔
84
    _is_fallback = False
1✔
85
    _schema_urls = tuple()
1✔
86
    _schemas = tuple()
1✔
87
    _expanded_schemas = tuple()
1✔
88

89
    @run_once_for_caller('_scheming_get_helpers', dict)
1✔
90
    def get_helpers(self):
1✔
91
        return dict(helpers.all_helpers)
1✔
92

93
    @run_once_for_caller('_scheming_get_validators', dict)
1✔
94
    def get_validators(self):
1✔
95
        return dict(validation.all_validators)
1✔
96

97
    @run_once_for_caller('_scheming_add_template_directory', lambda: None)
1✔
98
    def _add_template_directory(self, config):
1✔
99
        if not check_ckan_version('2.9'):
1✔
100
            add_template_directory(config, '2.8_templates')
×
101
        add_template_directory(config, 'templates')
1✔
102
        add_resource('assets', 'ckanext-scheming')
1✔
103

104
    @staticmethod
1✔
105
    def _load_presets(config):
1✔
106
        if _SchemingMixin._presets is not None:
1✔
107
            return
1✔
108

109
        presets = reversed(
1✔
110
            config.get(
111
                'scheming.presets',
112
                DEFAULT_PRESETS
113
            ).split()
114
        )
115

116
        _SchemingMixin._presets = {
1✔
117
            field['preset_name']: field['values']
118
            for preset_path in presets
119
            for field in _load_schema(preset_path)['presets']
120
        }
121

122
    def update_config(self, config):
1✔
123
        if self.instance:
1✔
124
            # reloading plugins, probably in WebTest
125
            _SchemingMixin._helpers_loaded = False
1✔
126
            _SchemingMixin._validators_loaded = False
1✔
127
        # record our plugin instance in a place where our helpers
128
        # can find it:
129
        self._store_instance(self)
1✔
130
        self._add_template_directory(config)
1✔
131

132
        # FIXME: need to read configuration in update_config
133
        # because self._schemas need to be defined early for
134
        # IDatasetForm
135
        self._load_presets(config)
1✔
136
        self._is_fallback = p.toolkit.asbool(
1✔
137
            config.get(self.FALLBACK_OPTION, False)
138
        )
139

140
        self._schema_urls = config.get(self.SCHEMA_OPTION, "").split()
1✔
141
        self._schemas = _load_schemas(
1✔
142
            self._schema_urls,
143
            self.SCHEMA_TYPE_FIELD
144
        )
145

146
        self._expanded_schemas = _expand_schemas(self._schemas)
1✔
147

148
    def is_fallback(self):
1✔
149
        return self._is_fallback
1✔
150

151

152
class _GroupOrganizationMixin(object):
1✔
153
    """
154
    Common methods for SchemingGroupsPlugin and SchemingOrganizationsPlugin
155
    """
156

157
    def group_types(self):
1✔
158
        return list(self._schemas)
1✔
159

160
    def setup_template_variables(self, context, data_dict):
1✔
161
        group_type = data_dict.get('type')
1✔
162
        if not group_type:
1✔
163
            if c.group_dict:
×
164
                group_type = c.group_dict['type']
×
165
            else:
166
                group_type = self.UNSPECIFIED_GROUP_TYPE
×
167
        c.scheming_schema = self._schemas[group_type]
1✔
168
        c.group_type = group_type
1✔
169
        c.scheming_fields = c.scheming_schema['fields']
1✔
170

171
    def validate(self, context, data_dict, schema, action):
1✔
172
        thing, action_type = action.split('_')
1✔
173
        t = data_dict.get('type', self.UNSPECIFIED_GROUP_TYPE)
1✔
174
        if not t or t not in self._schemas:
1✔
175
            return data_dict, {'type': "Unsupported {thing} type: {t}".format(
1✔
176
                thing=thing, t=t)}
177
        scheming_schema = self._expanded_schemas[t]
1✔
178
        scheming_fields = scheming_schema['fields']
1✔
179

180
        get_validators = (
1✔
181
            _field_output_validators_group
182
            if action_type == 'show' else _field_validators
183
        )
184
        for f in scheming_fields:
1✔
185
            schema[f['field_name']] = get_validators(
1✔
186
                f,
187
                scheming_schema,
188
                f['field_name'] not in schema
189
            )
190

191
        return navl_validate(data_dict, schema, context)
1✔
192

193

194
class SchemingDatasetsPlugin(p.SingletonPlugin, DefaultDatasetForm,
1✔
195
                             _SchemingMixin):
196
    p.implements(p.IConfigurer)
1✔
197
    p.implements(p.IConfigurable)
1✔
198
    p.implements(p.ITemplateHelpers)
1✔
199
    p.implements(p.IDatasetForm, inherit=True)
1✔
200
    p.implements(p.IActions)
1✔
201
    p.implements(p.IValidators)
1✔
202

203
    SCHEMA_OPTION = 'scheming.dataset_schemas'
1✔
204
    FALLBACK_OPTION = 'scheming.dataset_fallback'
1✔
205
    SCHEMA_TYPE_FIELD = 'dataset_type'
1✔
206

207
    @classmethod
1✔
208
    def _store_instance(cls, self):
1✔
209
        SchemingDatasetsPlugin.instance = self
1✔
210

211
    def read_template(self):
1✔
212
        return 'scheming/package/read.html'
1✔
213

214
    def resource_template(self):
1✔
215
        return 'scheming/package/resource_read.html'
1✔
216

217
    def package_form(self):
1✔
218
        return 'scheming/package/snippets/package_form.html'
1✔
219

220
    def resource_form(self):
1✔
221
        return 'scheming/package/snippets/resource_form.html'
1✔
222

223
    def package_types(self):
1✔
224
        return list(self._schemas)
1✔
225

226
    def validate(self, context, data_dict, schema, action):
1✔
227
        """
228
        Validate and convert for package_create, package_update and
229
        package_show actions.
230
        """
231
        thing, action_type = action.split('_')
1✔
232
        t = data_dict.get('type')
1✔
233
        if not t or t not in self._schemas:
1✔
234
            return data_dict, {'type': [
1✔
235
                "Unsupported dataset type: {t}".format(t=t)]}
236

237
        scheming_schema = self._expanded_schemas[t]
1✔
238

239
        before = scheming_schema.get('before_validators')
1✔
240
        after = scheming_schema.get('after_validators')
1✔
241
        if action_type == 'show':
1✔
242
            get_validators = _field_output_validators
1✔
243
            before = after = None
1✔
244
        elif action_type == 'create':
1✔
245
            get_validators = _field_create_validators
1✔
246
        else:
247
            get_validators = _field_validators
1✔
248

249
        if before:
1✔
250
            schema['__before'] = validation.validators_from_string(
×
251
                before, None, scheming_schema)
252
        if after:
1✔
253
            schema['__after'] = validation.validators_from_string(
×
254
                after, None, scheming_schema)
255
        fg = (
1✔
256
            (scheming_schema['dataset_fields'], schema, True),
257
            (scheming_schema['resource_fields'], schema['resources'], False)
258
        )
259

260
        composite_convert_fields = []
1✔
261
        for field_list, destination, is_dataset in fg:
1✔
262
            for f in field_list:
1✔
263
                convert_this = is_dataset and f['field_name'] not in schema
1✔
264
                destination[f['field_name']] = get_validators(
1✔
265
                    f,
266
                    scheming_schema,
267
                    convert_this
268
                )
269
                if convert_this and 'repeating_subfields' in f:
1✔
270
                    composite_convert_fields.append(f['field_name'])
1✔
271

272
        def composite_convert_to(key, data, errors, context):
1✔
273
            unflat = unflatten(data)
1✔
274
            for f in composite_convert_fields:
1✔
275
                if f not in unflat:
1✔
276
                    continue
1✔
277
                data[(f,)] = json.dumps(unflat[f], default=lambda x:None if x == missing else x)
1✔
278
                convert_to_extras((f,), data, errors, context)
1✔
279
                del data[(f,)]
1✔
280

281
        if action_type == 'show':
1✔
282
            if composite_convert_fields:
1✔
283
                for ex in data_dict['extras']:
1✔
284
                    if ex['key'] in composite_convert_fields:
1✔
285
                        data_dict[ex['key']] = json.loads(ex['value'])
1✔
286
                data_dict['extras'] = [
1✔
287
                    ex for ex in data_dict['extras']
288
                    if ex['key'] not in composite_convert_fields
289
                ]
290
        else:
291
            dataset_composite = {
1✔
292
                f['field_name']
293
                for f in scheming_schema['dataset_fields']
294
                if 'repeating_subfields' in f
295
            }
296
            if dataset_composite:
1✔
297
                expand_form_composite(data_dict, dataset_composite)
1✔
298
            resource_composite = {
1✔
299
                f['field_name']
300
                for f in scheming_schema['resource_fields']
301
                if 'repeating_subfields' in f
302
            }
303
            if resource_composite and 'resources' in data_dict:
1✔
304
                for res in data_dict['resources']:
1✔
305
                    expand_form_composite(res, resource_composite.copy())
1✔
306
            # convert composite package fields to extras so they are stored
307
            if composite_convert_fields:
1✔
308
                schema = dict(
1✔
309
                    schema,
310
                    __after=schema.get('__after', []) + [composite_convert_to])
311

312
        return navl_validate(data_dict, schema, context)
1✔
313

314
    def get_actions(self):
1✔
315
        """
316
        publish dataset schemas
317
        """
318
        return {
1✔
319
            'scheming_dataset_schema_list': logic.scheming_dataset_schema_list,
320
            'scheming_dataset_schema_show': logic.scheming_dataset_schema_show,
321
        }
322

323
    def setup_template_variables(self, context, data_dict):
1✔
324
        super(SchemingDatasetsPlugin, self).setup_template_variables(
1✔
325
            context, data_dict)
326
        # do not override licenses if they were already added by some
327
        # other extension. We just want to make sure, that licenses
328
        # are not empty.
329
        if not hasattr(c, 'licenses'):
1✔
330
            c.licenses = [('', '')] + model.Package.get_license_options()
1✔
331

332
    def configure(self, config):
1✔
333
        self._dataset_form_pages = {}
1✔
334

335
        for t, schema in self._expanded_schemas.items():
1✔
336
            pages = []
1✔
337
            self._dataset_form_pages[t] = pages
1✔
338

339
            for f in schema['dataset_fields']:
1✔
340
                if not pages or 'start_form_page' in f:
1✔
341
                    fp = f.get('start_form_page', {})
1✔
342
                    pages.append({
1✔
343
                        'title': fp.get('title', ''),
344
                        'description': fp.get('description', ''),
345
                        'fields': [],
346
                    })
347
                pages[-1]['fields'].append(f)
1✔
348

349
            if len(pages) == 1 and not pages[0]['title']:
1✔
350
                # no pages defined
351
                pages[:] = []
1✔
352

353
    def prepare_dataset_blueprint(self, package_type, bp):
1✔
354
        if self._dataset_form_pages[package_type]:
1✔
355
            bp.add_url_rule(
×
356
                '/new',
357
                'scheming_new',
358
                views.SchemingCreateView.as_view('new'),
359
            )
360
            bp.add_url_rule(
×
361
                '/new/<id>/<page>',
362
                'scheming_new_page',
363
                views.SchemingCreatePageView.as_view('new_page'),
364
            )
365
            bp.add_url_rule(
×
366
                '/edit/<id>',
367
                'scheming_edit',
368
                views.edit,
369
            )
370
            bp.add_url_rule(
×
371
                '/edit/<id>/<page>',
372
                'scheming_edit_page',
373
                views.SchemingEditPageView.as_view('edit_page'),
374
            )
375
        return bp
1✔
376

377

378
def expand_form_composite(data, fieldnames):
1✔
379
    """
380
    when submitting dataset/resource form composite fields look like
381
    "field-0-subfield..." convert these to lists of dicts
382
    """
383
    # if "field" exists, don't look for "field-0-subfield"
384
    fieldnames -= set(data)
1✔
385
    if not fieldnames:
1✔
386
        return
1✔
387
    indexes = {}
1✔
388
    for key in sorted(data):
1✔
389
        if '-' not in key:
1✔
390
            continue
1✔
391
        parts = key.split('-')
1✔
392
        if parts[0] not in fieldnames:
1✔
393
            continue
×
394
        if parts[1] not in indexes:
1✔
395
            indexes[parts[1]] = len(indexes)
1✔
396
        comp = data.setdefault(parts[0], [])
1✔
397
        parts[1] = indexes[parts[1]]
1✔
398
        try:
1✔
399
            try:
1✔
400
                comp[int(parts[1])]['-'.join(parts[2:])] = data[key]
1✔
401
                del data[key]
1✔
402
            except IndexError:
1✔
403
                comp.append({})
1✔
404
                comp[int(parts[1])]['-'.join(parts[2:])] = data[key]
1✔
405
                del data[key]
1✔
406
        except (IndexError, ValueError):
×
407
            pass  # best-effort only
×
408

409

410

411
class SchemingGroupsPlugin(p.SingletonPlugin, _GroupOrganizationMixin,
1✔
412
                           DefaultGroupForm, _SchemingMixin):
413
    p.implements(p.IConfigurer)
1✔
414
    p.implements(p.ITemplateHelpers)
1✔
415
    p.implements(p.IGroupForm, inherit=True)
1✔
416
    p.implements(p.IActions)
1✔
417
    p.implements(p.IValidators)
1✔
418

419
    SCHEMA_OPTION = 'scheming.group_schemas'
1✔
420
    FALLBACK_OPTION = 'scheming.group_fallback'
1✔
421
    SCHEMA_TYPE_FIELD = 'group_type'
1✔
422
    UNSPECIFIED_GROUP_TYPE = 'group'
1✔
423

424
    @classmethod
1✔
425
    def _store_instance(cls, self):
1✔
426
        SchemingGroupsPlugin.instance = self
1✔
427

428
    def about_template(self):
1✔
429
        return 'scheming/group/about.html'
1✔
430

431
    def group_form(self, group_type=None):
1✔
432
        return 'scheming/group/group_form.html'
1✔
433

434
    def get_actions(self):
1✔
435
        return {
1✔
436
            'scheming_group_schema_list': logic.scheming_group_schema_list,
437
            'scheming_group_schema_show': logic.scheming_group_schema_show,
438
        }
439

440

441
class SchemingOrganizationsPlugin(p.SingletonPlugin, _GroupOrganizationMixin,
1✔
442
                                  DefaultOrganizationForm, _SchemingMixin):
443
    p.implements(p.IConfigurer)
1✔
444
    p.implements(p.ITemplateHelpers)
1✔
445
    p.implements(p.IGroupForm, inherit=True)
1✔
446
    p.implements(p.IActions)
1✔
447
    p.implements(p.IValidators)
1✔
448

449
    SCHEMA_OPTION = 'scheming.organization_schemas'
1✔
450
    FALLBACK_OPTION = 'scheming.organization_fallback'
1✔
451
    SCHEMA_TYPE_FIELD = 'organization_type'
1✔
452
    UNSPECIFIED_GROUP_TYPE = 'organization'
1✔
453

454
    @classmethod
1✔
455
    def _store_instance(cls, self):
1✔
456
        SchemingOrganizationsPlugin.instance = self
1✔
457

458
    def about_template(self):
1✔
459
        return 'scheming/organization/about.html'
1✔
460

461
    def group_form(self, group_type=None):
1✔
462
        return 'scheming/organization/group_form.html'
1✔
463

464
    # use the correct controller (see ckan/ckan#2771)
465
    def group_controller(self):
1✔
466
        return 'organization'
1✔
467

468
    def get_actions(self):
1✔
469
        return {
1✔
470
            'scheming_organization_schema_list':
471
                logic.scheming_organization_schema_list,
472
            'scheming_organization_schema_show':
473
                logic.scheming_organization_schema_show,
474
        }
475

476

477
class SchemingNerfIndexPlugin(p.SingletonPlugin):
1✔
478
    """
479
    json.dump repeating dataset fields in before_index to prevent failures
480
    on unmodified solr schema. It's better to customize your solr schema
481
    and before_index processing than to use this plugin.
482
    """
483
    p.implements(p.IPackageController, inherit=True)
1✔
484

485
    def before_dataset_index(self, data_dict):
1✔
486
        return self.before_index(data_dict)
1✔
487

488
    def before_index(self, data_dict):
1✔
489
        schemas = SchemingDatasetsPlugin.instance._expanded_schemas
1✔
490
        if data_dict['type'] not in schemas:
1✔
491
            return data_dict
×
492

493
        for d in schemas[data_dict['type']]['dataset_fields']:
1✔
494
            if d['field_name'] not in data_dict:
1✔
495
                continue
1✔
496
            if 'repeating_subfields' in d:
1✔
497
                data_dict[d['field_name']] = json.dumps(data_dict[d['field_name']])
1✔
498

499
        return data_dict
1✔
500

501

502
def _load_schemas(schemas, type_field):
1✔
503
    out = {}
1✔
504
    for n in schemas:
1✔
505
        schema = _load_schema(n)
1✔
506
        out[schema[type_field]] = schema
1✔
507
    return out
1✔
508

509

510
def _load_schema(url):
1✔
511
    schema = _load_schema_module_path(url)
1✔
512
    if not schema:
1✔
513
        schema = _load_schema_url(url)
1✔
514
    return schema
1✔
515

516

517
def _load_schema_module_path(url):
1✔
518
    """
519
    Given a path like "ckanext.spatialx:spatialx_schema.json"
520
    find the second part relative to the import path of the first
521
    """
522

523
    module, file_name = url.split(':', 1)
1✔
524
    try:
1✔
525
        # __import__ has an odd signature
526
        m = __import__(module, fromlist=[''])
1✔
527
    except ImportError:
1✔
528
        return
1✔
529

530
    p = os.path.join(os.path.dirname(inspect.getfile(m)), file_name)
1✔
531
    if os.path.exists(p):
1✔
532
        if watch_file:
1✔
533
            watch_file(p)
×
534
        with open(p) as schema_file:
1✔
535
            return loader.load(schema_file)
1✔
536

537

538
def _load_schema_url(url):
1✔
539
    from six.moves import urllib
1✔
540
    try:
1✔
541
        res = urllib.request.urlopen(url)
1✔
542
        tables = res.read()
1✔
543
    except urllib.error.URLError:
1✔
544
        raise SchemingException("Could not load %s" % url)
1✔
545

546
    return loader.loads(tables, url)
1✔
547

548

549
def _field_output_validators_group(f, schema, convert_extras):
1✔
550
    """
551
    Return the output validators for a scheming field f, tailored for groups
552
    and orgs.
553
    """
554
    return _field_output_validators(
1✔
555
        f,
556
        schema,
557
        convert_extras,
558
        convert_from_extras_type=validation.convert_from_extras_group
559
    )
560

561

562
def _field_output_validators(f, schema, convert_extras,
1✔
563
                             convert_from_extras_type=convert_from_extras):
564
    """
565
    Return the output validators for a scheming field f
566
    """
567
    if 'repeating_subfields' in f:
1✔
568
        validators = {
1✔
569
            sf['field_name']: _field_output_validators(sf, schema, False)
570
            for sf in f['repeating_subfields']
571
        }
572
    elif convert_extras:
1✔
573
        validators = [convert_from_extras_type, ignore_missing]
1✔
574
    else:
575
        validators = [ignore_missing]
1✔
576
    if 'output_validators' in f:
1✔
577
        validators += validation.validators_from_string(
1✔
578
            f['output_validators'], f, schema)
579
    return validators
1✔
580

581

582
def _field_validators(f, schema, convert_extras):
1✔
583
    """
584
    Return the validators for a scheming field f
585
    """
586
    if 'validators' in f:
1✔
587
        validators = validation.validators_from_string(
1✔
588
            f['validators'],
589
            f,
590
            schema
591
        )
592
    elif helpers.scheming_field_required(f):
1✔
593
        validators = [not_empty]
1✔
594
    else:
595
        validators = [ignore_missing]
1✔
596

597
    if convert_extras:
1✔
598
        validators.append(convert_to_extras)
1✔
599

600
    # If this field contains children, we need a special validator to handle
601
    # them.
602
    if 'repeating_subfields' in f:
1✔
603
        validators = {
1✔
604
            sf['field_name']: _field_validators(sf, schema, False)
605
            for sf in f['repeating_subfields']
606
        }
607

608
    return validators
1✔
609

610

611
def _field_create_validators(f, schema, convert_extras):
1✔
612
    """
613
    Return the validators to use when creating for scheming field f,
614
    normally the same as the validators used for updating
615
    """
616
    if 'create_validators' not in f:
1✔
617
        return _field_validators(f, schema, convert_extras)
1✔
618

619
    validators = validation.validators_from_string(
×
620
        f['create_validators'],
621
        f,
622
        schema
623
    )
624

625
    if convert_extras:
×
626
        validators.append(convert_to_extras)
×
627

628
    # If this field contains children, we need a special validator to handle
629
    # them.
630
    if 'repeating_subfields' in f:
×
631
        validators = {
×
632
            sf['field_name']: _field_create_validators(sf, schema, False)
633
            for sf in f['repeating_subfields']
634
        }
635

636
    return validators
×
637

638

639
def _expand(schema, field):
1✔
640
    """
641
    If scheming field f includes a preset value return a new field
642
    based on the preset with values from f overriding any values in the
643
    preset.
644

645
    raises SchemingException if the preset given is not found.
646
    """
647
    preset = field.get('preset')
1✔
648
    if preset:
1✔
649
        if preset not in _SchemingMixin._presets:
1✔
650
            raise SchemingException('preset \'{}\' not defined'.format(preset))
×
651
        field = dict(_SchemingMixin._presets[preset], **field)
1✔
652

653
    return field
1✔
654

655

656
def _expand_schemas(schemas):
1✔
657
    """
658
    Return a new dict of schemas with all field presets expanded.
659
    """
660
    out = {}
1✔
661
    for name, original in schemas.items():
1✔
662
        schema = dict(original)
1✔
663
        for grouping in ('fields', 'dataset_fields', 'resource_fields'):
1✔
664
            if grouping not in schema:
1✔
665
                continue
1✔
666

667
            schema[grouping] = [
1✔
668
                _expand(schema, field)
669
                for field in schema[grouping]
670
            ]
671

672
            for field in schema[grouping]:
1✔
673
                if 'repeating_subfields' in field:
1✔
674
                    field['repeating_subfields'] = [
1✔
675
                        _expand(schema, subfield)
676
                        for subfield in field['repeating_subfields']
677
                    ]
678
                elif 'simple_subfields' in field:
1✔
679
                    field['simple_subfields'] = [
×
680
                        _expand(schema, subfield)
681
                        for subfield in field['simple_subfields']
682
                    ]
683

684
        out[name] = schema
1✔
685
    return out
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc