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

woudc / woudc-data-registry / 14226496828

02 Apr 2025 06:07PM UTC coverage: 50.841% (-0.7%) from 51.5%
14226496828

Pull #104

github

web-flow
Merge 4c22dbdaa into df78ba793
Pull Request #104: Enhance Product Generation:

1 of 155 new or added lines in 4 files covered. (0.65%)

11 existing lines in 3 files now uncovered.

3175 of 6245 relevant lines covered (50.84%)

0.51 hits per line

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

6.01
/woudc_data_registry/processing.py
1
# =================================================================
2
#
3
# Terms and Conditions of Use
4
#
5
# Unless otherwise noted, computer program source code of this
6
# distribution # is covered under Crown Copyright, Government of
7
# Canada, and is distributed under the MIT License.
8
#
9
# The Canada wordmark and related graphics associated with this
10
# distribution are protected under trademark law and copyright law.
11
# No permission is granted to use them outside the parameters of
12
# the Government of Canada's corporate identity program. For
13
# more information, see
14
# http://www.tbs-sct.gc.ca/fip-pcim/index-eng.asp
15
#
16
# Copyright title to all 3rd party software distributed with this
17
# software is held by the respective copyright holders as noted in
18
# those files. Users are asked to read the 3rd Party Licenses
19
# referenced with those assets.
20
#
21
# Copyright (c) 2024 Government of Canada
22
#
23
# Permission is hereby granted, free of charge, to any person
24
# obtaining a copy of this software and associated documentation
25
# files (the "Software"), to deal in the Software without
26
# restriction, including without limitation the rights to use,
27
# copy, modify, merge, publish, distribute, sublicense, and/or sell
28
# copies of the Software, and to permit persons to whom the
29
# Software is furnished to do so, subject to the following
30
# conditions:
31
#
32
# The above copyright notice and this permission notice shall be
33
# included in all copies or substantial portions of the Software.
34
#
35
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
36
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
37
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
38
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
39
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
40
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
41
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
42
# OTHER DEALINGS IN THE SOFTWARE.
43
#
44
# =================================================================
45

46

47
import os
1✔
48
import yaml
1✔
49

50
import shutil
1✔
51
import logging
1✔
52

53
from datetime import datetime
1✔
54
from woudc_extcsv import DOMAINS
1✔
55

56
from woudc_data_registry import config
1✔
57
from woudc_data_registry.models import (Contributor, DataRecord, Dataset,
1✔
58
                                        Deployment, Instrument, Project,
59
                                        Station, StationName, Contribution)
60
from woudc_data_registry.dataset_validators import get_validator
1✔
61

62
from woudc_data_registry.epicentre.station import build_station_name
1✔
63
from woudc_data_registry.epicentre.instrument import build_instrument
1✔
64
from woudc_data_registry.epicentre.deployment import build_deployment
1✔
65

66

67
LOGGER = logging.getLogger(__name__)
1✔
68

69
with open(config.WDR_ALIAS_CONFIG) as alias_definitions:
1✔
70
    ALIASES = yaml.safe_load(alias_definitions)
1✔
71

72

73
class Process(object):
1✔
74
    """
75
    Generic processing definition
76

77
    - detect
78
    - parse
79
    - validate
80
    - verify
81
    - register
82
    - index
83
    """
84

85
    def __init__(self, registry_conn, search_index_conn, reporter):
1✔
86
        """constructor"""
87

88
        self.status = None
×
89
        self.code = None
×
90
        self.message = None
×
91
        self.process_start = datetime.utcnow()
×
92
        self.process_end = None
×
93

94
        self.registry = registry_conn
×
95
        self.search_index = search_index_conn
×
96
        self.reports = reporter
×
97

98
        self._registry_updates = []
×
99
        self._search_index_updates = []
×
100

101
        self.warnings = []
×
102
        self.errors = []
×
103

104
    def _add_to_report(self, error_code, line=None, **kwargs):
1✔
105
        """
106
        Submit a warning or error of code <error_code> to the report generator,
107
        with was found at line <line> in the input file. Uses keyword arguments
108
        to detail the warning/error message.
109

110
        Returns False iff the error is serious enough to abort parsing.
111
        """
112

113
        message, severe = self.reports.add_message(error_code, line, **kwargs)
×
114
        if severe:
×
115
            LOGGER.error(message)
×
116
            self.errors.append(message)
×
117
        else:
118
            LOGGER.warning(message)
×
119
            self.warnings.append(message)
×
120

121
        return not severe
×
122

123
    def validate(self, extcsv, metadata_only=False, verify_only=False,
1✔
124
                 bypass=False):
125
        """
126
        Process incoming data record.
127

128
        :param infile: Path to incoming data file.
129
        :param metadata_only: `bool` of whether to only verify common
130
                              metadata tables.
131
        :param verify_only: `bool` of whether to verify the file for
132
                            correctness without processing.
133
        :param bypass: `bool` of whether to skip permission prompts
134
                        to add records.
135
        :returns: `bool` of whether the operation was successful.
136
        """
137

138
        # detect incoming data file
139
        self.extcsv = extcsv
×
140

141
        self.warnings = []
×
142
        self.errors = []
×
143

144
        LOGGER.info('Verifying data record against core metadata fields')
×
145

146
        project_ok = self.check_project()
×
147
        dataset_ok = self.check_dataset()
×
148

149
        if not project_ok:
×
150
            LOGGER.warning('Skipping contributor check: depends on'
×
151
                           ' values with errors')
152
            contributor_ok = False
×
153
        else:
154
            contributor_ok = self.check_contributor()
×
155

156
        platform_ok = self.check_station(bypass=bypass, verify=verify_only)
×
157

158
        if not all([project_ok, contributor_ok, platform_ok]):
×
159
            LOGGER.warning('Skipping deployment check: depends on'
×
160
                           ' values with errors')
161
            deployment_ok = False
×
162
        else:
163
            LOGGER.debug('Validating agency deployment')
×
164
            deployment_ok = self.check_deployment()
×
165

166
            project = self.extcsv.extcsv['CONTENT']['Class']
×
167
            agency = self.extcsv.extcsv['DATA_GENERATION']['Agency']
×
168
            platform_id = str(self.extcsv.extcsv['PLATFORM']['ID'])
×
169

170
            if not deployment_ok:
×
171
                deployment_id = ':'.join([platform_id, agency, project])
×
172
                deployment_name = f'{agency}@{platform_id}'
×
173
                LOGGER.warning(f'Deployment {deployment_id} not found')
×
174

175
                if verify_only:
×
176
                    LOGGER.info('Verify mode. Skipping deployment addition.')
×
177
                    deployment_ok = True
×
178
                elif self.add_deployment(bypass=bypass):
×
179
                    deployment_ok = True
×
180

181
                    self._add_to_report(407)
×
182
                else:
183
                    msg = f'Deployment {deployment_name} not added. Skipping file.'  # noqa
×
184
                    LOGGER.warning(msg)
×
185

186
                    line = self.extcsv.line_num('PLATFORM') + 2
×
187
                    deployment_ok = self._add_to_report(334, line,
×
188
                                                        ident=deployment_id)
189

190
        LOGGER.debug('Validating instrument')
×
191
        if not all([dataset_ok, platform_ok]):
×
192
            LOGGER.warning('Skipping instrument check: depends on'
×
193
                           ' values with errors')
194
            instrument_ok = False
×
195
        else:
196
            instrument_model_ok = self.check_instrument_name_and_model()
×
197

198
            if not instrument_model_ok:
×
199
                LOGGER.warning('Instrument name and model failed to validate;'
×
200
                               ' aborting instrument checks')
201
                instrument_ok = False
×
202
            else:
203
                instrument_ok = self.check_instrument()
×
204

205
                if not instrument_ok:
×
206
                    # Attempt to fix the serial by left-stripping zeroes
207
                    old_serial = \
×
208
                        str(self.extcsv.extcsv['INSTRUMENT']['Number'])
209
                    new_serial = correct_instrument_value(old_serial, 'serial')
×
210

211
                    if old_serial != new_serial:
×
212
                        LOGGER.debug('Attempting to search instrument serial'
×
213
                                     f' number {new_serial}')
214

215
                        self.extcsv.extcsv['INSTRUMENT']['Number'] = new_serial
×
216
                        instrument_ok = self.check_instrument()
×
217

218
                if not instrument_ok:
×
219
                    # Attempt to add a new record with the new serial number
220
                    # using name and model from the registry
221
                    LOGGER.warning(f'No instrument with serial {old_serial} '
×
222
                                   'found in registry')
223
                    self.extcsv.extcsv['INSTRUMENT']['Number'] = old_serial
×
224

225
                    if verify_only:
×
226
                        LOGGER.info('Verify mode. Skipping instrument'
×
227
                                    ' addition.')
228
                        instrument_ok = True
×
229
                    else:
230
                        instrument_ok = self.add_instrument(bypass=bypass)
×
231

232
                    if instrument_ok:
×
233
                        self._add_to_report(406)
×
234

235
        if not instrument_ok:
×
236
            line = self.extcsv.line_num('INSTRUMENT') + 2
×
237
            instrument_ok = self._add_to_report(333, line)
×
238

239
            location_ok = False
×
240
        else:
241
            location_ok = self.check_location()
×
242

243
        LOGGER.info('Validating contribution')
×
244
        contribution_ok = True
×
245
        if not all([instrument_ok, project_ok,
×
246
                    dataset_ok,
247
                    platform_ok, location_ok]):
248
            LOGGER.warning('Contribution is not valid due to'
×
249
                           ' fields it depends on being invalid')
250
            contribution_ok = False
×
251

252
        if verify_only and contribution_ok:
×
253
            LOGGER.info('Verify mode. Skipping Contribution addition.')
×
254
        elif contribution_ok:
×
255
            contribution_exists = self.check_contribution()
×
256
            if not contribution_exists:
×
257
                contribution_ok = self.add_contribution(bypass=bypass)
×
258

259
            if contribution_ok and (not contribution_exists):
×
260
                self._add_to_report(409)
×
261

262
        content_ok = self.check_content()
×
263
        data_generation_ok = self.check_data_generation()
×
264

265
        if not all([project_ok, dataset_ok, contributor_ok,
×
266
                    platform_ok, deployment_ok, instrument_ok,
267
                    location_ok, content_ok, data_generation_ok,
268
                    contribution_ok]):
269
            self._add_to_report(410)
×
270
            return None
×
271

272
        if metadata_only:
×
273
            msg = 'Lax mode detected. NOT validating dataset-specific tables'
×
274
            LOGGER.info(msg)
×
275
        else:
276
            dataset_name = self.extcsv.extcsv['CONTENT']['Category']
×
277
            dataset_validator = get_validator(dataset_name, self.reports)
×
278

279
            time_series_ok = self.check_time_series()
×
280
            dataset_validated = dataset_validator.check_all(self.extcsv)
×
281

282
            if not all([time_series_ok, dataset_validated]):
×
283
                self._add_to_report(410)
×
284
                return None
×
285

286
        LOGGER.info('Validating data record')
×
287
        data_record = DataRecord(self.extcsv)
×
288
        data_record_ok = self.check_data_record(data_record)
×
289

290
        if not data_record_ok:
×
291
            self._add_to_report(410)
×
292
            return None
×
293
        else:
294
            LOGGER.info('Data record is valid and verified')
×
295
            self._registry_updates.append(data_record)
×
296
            self._search_index_updates.append(data_record)
×
297

298
            self._add_to_report(405)
×
299
            return data_record
×
300

301
    def persist(self):
1✔
302
        """
303
        Publish all changes from the previous file parse to the data registry
304
        and ElasticSearch index, including instrument/deployment updates.
305
        Copies the input file to the WAF.
306

307
        :returns: void
308
        """
309

310
        data_records = set()
×
311

312
        if not config.EXTRAS['processing']['registry_enabled']:
×
313
            LOGGER.info('Data registry persistence disabled, skipping.')
×
314
        else:
315
            LOGGER.info('Beginning persistence to data registry')
×
316
            for model in self._registry_updates:
×
317
                LOGGER.debug(f'Saving {model} to registry')
×
318
                self.registry.save(model)
×
319

320
                if isinstance(model, DataRecord):
×
321
                    data_records.add(model)
×
322

323
        if not config.EXTRAS['processing']['search_index_enabled']:
×
324
            LOGGER.info('Search index persistence disabled, skipping.')
×
325
        else:
326
            LOGGER.info('Beginning persistence to search index')
×
327
            for model in self._search_index_updates:
×
328
                if not isinstance(model, DataRecord):
×
329
                    allow_update_model = True
×
330
                else:
331
                    # Do not persist older versions of data records.
332
                    esid = model.es_id
×
333
                    prev_version = self.search_index.get_record_version(esid)
×
334
                    now_version = model.data_generation_version
×
335

336
                    if not prev_version or now_version > prev_version:
×
337
                        allow_update_model = True
×
338
                        data_records.add(model)
×
339
                    else:
340
                        allow_update_model = False
×
341

342
                if allow_update_model:
×
343
                    LOGGER.debug(f'Saving {model} to search index')
×
344
                    self.search_index.index(type(model),
×
345
                                            model.__geo_interface__)
346

347
        LOGGER.info('Saving data record CSVs to WAF')
×
348
        for record in data_records:
×
349
            waf_filepath = record.get_waf_path(config.WDR_WAF_BASEDIR)
×
350
            os.makedirs(os.path.dirname(waf_filepath), exist_ok=True)
×
351
            shutil.copy2(record.ingest_filepath, waf_filepath)
×
352

353
        LOGGER.info('Persistence complete')
×
354
        self._registry_updates = []
×
355
        self._search_index_updates = []
×
356

357
    def add_deployment(self, bypass=False):
1✔
358
        """
359
        Create a new deployment instance for the input Extended CSV file's
360
        #PLATFORM and #DATA_GENERATION.Agency. Queues the new deployment
361
        to be saved next time the publish method is called.
362

363
        Unless <bypass> is provided and True, there will be a permission
364
        prompt before a record is created. If permission is denied, no
365
        deployment will be queued and False will be returned.
366

367
        :param bypass: `bool` of whether to skip permission checks
368
                       to add the deployment.
369
        :returns: void
370
        """
371

372
        deployment = build_deployment(self.extcsv)
×
373

374
        if bypass:
×
375
            LOGGER.info('Bypass mode. Skipping permission check.')
×
376
            allow_add_deployment = True
×
377
        else:
378
            response = input(f'Deployment {deployment.deployment_id} not found. Add? (y/n) [n]: ')  # noqa
×
379
            allow_add_deployment = response.lower() in ['y', 'yes']
×
380

381
        if not allow_add_deployment:
×
382
            return False
×
383
        else:
384
            LOGGER.info('Queueing new deployment...')
×
385

386
            self._registry_updates.append(deployment)
×
387
            self._search_index_updates.append(deployment)
×
388
            return True
×
389

390
    def add_station_name(self, bypass=False):
1✔
391
        """
392
        Create an alternative station name for the input Extended CSV file's
393
        #PLATFORM.Name and #PLATFORM.ID. Queues the new station name
394
        record to be saved next time the publish method is called.
395

396
        Unless <bypass> is provided and True, there will be a permission
397
        prompt before a record is created. If permission is denied, no
398
        station name will be queued and False will be returned.
399

400
        :param bypass: `bool` of whether to skip permission checks
401
                       to add the name.
402
        :returns: `bool` of whether the operation was successful.
403
        """
404

405
        station_name_object = build_station_name(self.extcsv)
×
406

407
        if bypass:
×
408
            LOGGER.info('Bypass mode. Skipping permission check')
×
409
            allow_add_station_name = True
×
410
        else:
411
            response = input(f'Station name {station_name_object.station_name_id} not found. Add? (y/n) [n]: ')  # noqa
×
412
            allow_add_station_name = response.lower() in ['y', 'yes']
×
413

414
        if not allow_add_station_name:
×
415
            return False
×
416
        else:
417
            LOGGER.info('Queueing new station name...')
×
418

419
            self._registry_updates.append(station_name_object)
×
420
            return True
×
421

422
    def add_instrument(self, bypass=False):
1✔
423
        """
424
        Create a new instrument record from the input Extended CSV file's
425
        #INSTRUMENT table and queue it to be saved next time the publish
426
        method is called.
427

428
        Unless <bypass> is provided and True, there will be a permission
429
        prompt before a record is created. If permission is denied, no
430
        new instrument will be queued and False will be returned.
431

432
        :param bypass: `bool` of whether to skip permission checks
433
                       to add the instrument.
434
        :returns: `bool` of whether the operation was successful.
435
        """
436

437
        instrument = build_instrument(self.extcsv)
×
438

439
        if bypass:
×
440
            LOGGER.info('Bypass mode. Skipping permission check')
×
441
            allow_add_instrument = True
×
442
        else:
443
            response = input(f'Instrument {instrument.instrument_id} not found. Add? (y/n) [n]: ')  # noqa
×
444
            allow_add_instrument = response.lower() in ['y', 'yes']
×
445

446
        if allow_add_instrument:
×
447
            LOGGER.info('Queueing new instrument...')
×
448

449
            self._registry_updates.append(instrument)
×
450
            self._search_index_updates.append(instrument)
×
451
            return True
×
452
        else:
453
            return False
×
454

455
    def add_contribution(self, bypass=False):
1✔
456
        """
457
        Create a new contribution record from the input Extended CSV file's
458
        various fields and queue it to be saved the next time the publish
459
        method is called
460

461
        Unless <bypass> is provided and True, there will be a permission
462
        prompt before a record is created. If permission is denied, no
463
        new contribution will be queued and False will be returned.
464

465
        :param bypass: `bool` of whether to skip permission checks
466
                       to add the contribution.
467
        :returns: `bool` of whether the operation was successful.
468
        """
469

470
        project_id = self.extcsv.extcsv['CONTENT']['Class']
×
471
        dataset_name = self.extcsv.extcsv['CONTENT']['Category']
×
472
        dataset_level = float(self.extcsv.extcsv['CONTENT']['Level'])
×
473
        dataset_id = f"{dataset_name}_{dataset_level}"
×
474
        station_id = str(self.extcsv.extcsv['PLATFORM']['ID'])
×
475
        country_id = self.extcsv.extcsv['PLATFORM']['Country']
×
476

477
        agency = self.extcsv.extcsv['DATA_GENERATION']['Agency']
×
478

479
        instrument_name = self.extcsv.extcsv['INSTRUMENT']['Name']
×
480

481
        start_date = self.extcsv.extcsv['TIMESTAMP']['Date']
×
482
        end_date = None
×
483

484
        contributor_id = ':'.join([agency, project_id])
×
485
        contributor_from_registry = \
×
486
            self.registry.query_by_field(Contributor,
487
                                         'contributor_id', contributor_id)
488
        contributor_name = None
×
489
        if contributor_from_registry is not None:
×
490
            contributor_name = contributor_from_registry.name
×
491

492
        contribution_id = ':'.join([project_id, dataset_id,
×
493
                                    station_id, instrument_name])
494

495
        contribution_data = {'contribution_id': contribution_id,
×
496
                             'project_id': project_id,
497
                             'dataset_id': dataset_id,
498
                             'station_id': station_id,
499
                             'country_id': country_id,
500
                             'instrument_name': instrument_name,
501
                             'contributor_name': contributor_name,
502
                             'start_date': start_date,
503
                             'end_date': end_date,
504
                             }
505

506
        contribution = Contribution(contribution_data)
×
507

508
        if bypass:
×
509
            LOGGER.info('Bypass mode. Skipping permission check')
×
510
            allow_add_contribution = True
×
511
        else:
512
            response = input(f'Contribution {contribution.contribution_id} not found. Add? (y/n) [n]: ')  # noqa
×
513
            allow_add_contribution = response.lower() in ['y', 'yes']
×
514

515
        if allow_add_contribution:
×
516
            LOGGER.info('Queueing new contribution...')
×
517
            self._registry_updates.append(contribution)
×
518
            self._search_index_updates.append(contribution)
×
519
            return True
×
520
        else:
521
            return False
×
522

523
    def check_contribution(self):
1✔
524
        """
525
        Checks if a contribution object with an id created using
526
        the Extended CSV source file's fields already exists
527
        within the data registry
528
        """
529

530
        project_id = self.extcsv.extcsv['CONTENT']['Class']
×
531
        dataset_name = self.extcsv.extcsv['CONTENT']['Category']
×
532
        dataset_level = float(self.extcsv.extcsv['CONTENT']['Level'])
×
533
        dataset_id = f"{dataset_name}_{dataset_level}"
×
534
        station_id = str(self.extcsv.extcsv['PLATFORM']['ID'])
×
535

536
        timestamp_date = self.extcsv.extcsv['TIMESTAMP']['Date']
×
537

538
        instrument_name = self.extcsv.extcsv['INSTRUMENT']['Name']
×
539

540
        contribution_id = ':'.join([project_id, dataset_id,
×
541
                                    station_id, instrument_name])
542

543
        contribution = self.registry.query_by_field(Contribution,
×
544
                                                    'contribution_id',
545
                                                    contribution_id)
546
        if not contribution:
×
547
            LOGGER.warning(f'Contribution {contribution_id} not found')
×
548
            return False
×
549
        else:
550
            LOGGER.warning(f'Found contribution match for {contribution_id}')
×
551
            if not isinstance(timestamp_date, (str, int)):
×
552
                if contribution.start_date > timestamp_date:
×
553
                    contribution.start_date = timestamp_date
×
554
                    self._registry_updates.append(contribution)
×
555
                    LOGGER.debug('Contribution start date updated')
×
556
                elif contribution.end_date  \
×
557
                        and contribution.end_date < timestamp_date:
558
                    contribution.end_date = timestamp_date
×
559
                    self._registry_updates.append(contribution)
×
560
                    LOGGER.debug('Contribution end date updated')
×
561
            return True
×
562

563
    def check_project(self):
1✔
564
        """
565
        Validates the instance's Extended CSV source file's #CONTENT.Class,
566
        and returns True if no errors are found.
567

568
        :returns: `bool` of whether the input file's project
569
                  validated successfully.
570
        """
571

572
        project = self.extcsv.extcsv['CONTENT']['Class']
×
573

574
        LOGGER.debug(f'Validating project {project}')
×
575
        self.projects = self.registry.query_distinct(Project.project_id)
×
576

577
        if project in self.projects:
×
578
            LOGGER.debug(f'Match found for project {project}')
×
579
            return True
×
580
        else:
581
            line = self.extcsv.line_num('CONTENT') + 2
×
582
            return self._add_to_report(307, line, value=project)
×
583

584
    def check_dataset(self):
1✔
585
        """
586
        Validates the instance's Extended CSV source file's #CONTENT.Category,
587
        and returns True if no errors are found.
588

589
        Adjusts the Extended CSV contents if necessary to form a match.
590

591
        :returns: `bool` of whether the input file's dataset
592
                  validated successfully.
593
        """
594

595
        dataset_name = self.extcsv.extcsv['CONTENT']['Category']
×
596
        dataset_level = float(self.extcsv.extcsv['CONTENT']['Level'])
×
597
        dataset_id = f"{dataset_name}_{dataset_level}"
×
598

599
        LOGGER.debug(f'Validating dataset {dataset_id}')
×
600
        dataset_model = {'dataset_id': dataset_id}
×
601

602
        fields = ['dataset_id']
×
603
        response = self.registry.query_multiple_fields(Dataset, dataset_model,
×
604
                                                       fields, fields)
605
        if response:
×
606
            LOGGER.debug(f'Match found for dataset {dataset_id}')
×
607
            self.extcsv.extcsv['CONTENT']['Category'] = (
×
608
                response.dataset_id.rsplit('_', 1)[0]
609
            )
610
            return True
×
611
        else:
612
            line = self.extcsv.line_num('CONTENT') + 2
×
613
            return self._add_to_report(308, line, value=dataset_id)
×
614

615
    def check_contributor(self):
1✔
616
        """
617
        Validates the instance's Extended CSV source file's
618
        #DATA_GENERATION.Agency, and returns True if no errors are found.
619

620
        Adjusts the Extended CSV contents if necessary to form a match.
621

622
        Prerequisite: #CONTENT.Class is a trusted value.
623

624
        :returns: `bool` of whether the input file's contributor
625
                  validated successfully.
626
        """
627

628
        success = True
×
629

630
        agency = self.extcsv.extcsv['DATA_GENERATION']['Agency']
×
631
        project = self.extcsv.extcsv['CONTENT']['Class']
×
632

633
        if agency in ALIASES['Agency']:
×
634
            line = self.extcsv.line_num('DATA_GENERATION') + 2
×
635
            replacement = ALIASES['Agency'][agency]
×
636

637
            if not isinstance(replacement, str):
×
638
                if not self._add_to_report(108, line, replacement):
×
639
                    success = False
×
640

641
            agency = replacement
×
642
            self.extcsv.extcsv['DATA_GENERATION']['Agency'] = agency
×
643

644
        LOGGER.debug(f'Validating contributor {agency} under project {project}')  # noqa
×
645
        contributor = {
×
646
            'contributor_id': f'{agency}:{project}',
647
            'project_id': project
648
        }
649

650
        fields = ['contributor_id']
×
651
        result = self.registry.query_multiple_fields(Contributor, contributor,
×
652
                                                     fields, fields)
653
        if result:
×
654
            contributor_name = result.acronym
×
655
            self.extcsv.extcsv['DATA_GENERATION']['Agency'] = contributor_name
×
656

657
            LOGGER.debug(f'Match found for contributor ID {result.contributor_id}')  # noqa
×
658
        else:
659
            line = self.extcsv.line_num('DATA_GENERATION') + 2
×
660
            if not self._add_to_report(317, line):
×
661
                success = False
×
662

663
        return success
×
664

665
    def check_station(self, bypass=False, verify=False):
1✔
666
        """
667
        Validates the instance's Extended CSV source file's #PLATFORM table
668
        and returns True if no errors are found.
669

670
        Adjusts the Extended CSV contents if necessary to form a match.
671

672
        :param bypass: `bool` of whether to skip permission prompts
673
                        to add records.
674
        :param verify_only: `bool` of whether to verify the file for
675
                            correctness without processing.
676
        :returns: `bool` of whether the input file's station
677
                  validated successfully.
678
        """
679

680
        success = True
×
681

682
        identifier = str(self.extcsv.extcsv['PLATFORM']['ID'])
×
683
        pl_type = self.extcsv.extcsv['PLATFORM']['Type']
×
684
        name = self.extcsv.extcsv['PLATFORM']['Name']
×
685
        country = self.extcsv.extcsv['PLATFORM']['Country']
×
686
        # gaw_id = self.extcsv.extcsv['PLATFORM'].get('GAW_ID')
687

688
        # TODO: consider adding and checking #PLATFORM_Type
689
        LOGGER.debug(f'Validating station {identifier}:{name}')
×
690
        valueline = self.extcsv.line_num('PLATFORM') + 2
×
691

692
        water_codes = ['*IW', 'IW', 'XZ']
×
693
        if pl_type == 'SHP' and any([not country, country in water_codes]):
×
694
            if not self._add_to_report(323, valueline):
×
695
                success = False
×
696

697
            self.extcsv.extcsv['PLATFORM']['Country'] = country = 'XY'
×
698

699
        if len(identifier) < 3:
×
700
            if not self._add_to_report(318, valueline):
×
701
                success = False
×
702

703
            identifier = identifier.rjust(3, '0')
×
704
            self.extcsv.extcsv['PLATFORM']['ID'] = identifier
×
705

706
        station = {
×
707
            'station_id': identifier,
708
            'station_type': pl_type,
709
            'current_name': name,
710
            'country_id': country
711
        }
712

713
        LOGGER.debug('Validating station id...')
×
714
        response = self.registry.query_by_field(Station, 'station_id',
×
715
                                                identifier)
716
        if response:
×
717
            LOGGER.debug(f'Validated station with id: {identifier}')
×
718
        else:
719
            self._add_to_report(319, valueline)
×
720
            return False
×
721

722
        LOGGER.debug('Validating station type...')
×
723
        platform_types = ['STN', 'SHP']
×
724
        type_ok = pl_type in platform_types
×
725

726
        if type_ok:
×
727
            LOGGER.debug(f'Validated station type {type_ok}')
×
728
        elif not self._add_to_report(320, valueline):
×
729
            success = False
×
730

731
        LOGGER.debug('Validating station name...')
×
732
        model = {'station_id': identifier, 'name': station['current_name']}
×
733
        response = self.registry.query_multiple_fields(StationName, model,
×
734
                                                       model.keys(), ['name'])
735
        name_ok = bool(response)
×
736
        if name_ok:
×
737
            self.extcsv.extcsv['PLATFORM']['Name'] = name = response.name
×
738
            LOGGER.debug(f'Validated with name {name} for id {identifier}')
×
739
        elif verify:
×
740
            LOGGER.info('Verify mode. Skipping station name addition.')
×
741
        elif self.add_station_name(bypass=bypass):
×
742
            LOGGER.info(f"Added new station name {station['current_name']}")
×
743
        elif not self._add_to_report(321, valueline, name=name):
×
744
            success = False
×
745

746
        LOGGER.debug('Validating station country...')
×
747
        fields = ['station_id', 'country_id']
×
748
        response = self.registry.query_multiple_fields(Station, station,
×
749
                                                       fields, ['country_id'])
750
        country_ok = bool(response)
×
751
        if country_ok:
×
752
            country = response.country
×
753
            self.extcsv.extcsv['PLATFORM']['Country'] = country.country_id
×
754
            LOGGER.debug(f'Validated with country: {country.name_en} ({country.country_id}) for id: {identifier}')  # noqa
×
755
        elif not self._add_to_report(322, valueline):
×
756
            success = False
×
757

758
        return success
×
759

760
    def check_deployment(self):
1✔
761
        """
762
        Validates the instance's Extended CSV source file's combination of
763
        #DATA_GENERATION.Agency and #PLATFORM.ID, and returns True if no
764
        errors are found.
765

766
        Updates the deployment's start and end date if a match is found.
767

768
        Prerequisite: #DATA_GENERATION.Agency,
769
                      #PLATFORM_ID, and
770
                      #CONTENT.Class are all trusted values.
771

772
        :returns: `bool` of whether the input file's station-contributor
773
                  pairing validated successfully.
774
        """
775

776
        station = str(self.extcsv.extcsv['PLATFORM']['ID'])
×
777
        agency = self.extcsv.extcsv['DATA_GENERATION']['Agency']
×
778
        project = self.extcsv.extcsv['CONTENT']['Class']
×
779
        timestamp_date = self.extcsv.extcsv['TIMESTAMP']['Date']
×
780

781
        deployment_id = ':'.join([station, agency, project])
×
782
        deployment = self.registry.query_by_field(Deployment, 'deployment_id',
×
783
                                                  deployment_id)
784
        if not deployment:
×
785
            LOGGER.warning(f'Deployment {deployment_id} not found')
×
786
            return False
×
787
        else:
788
            LOGGER.debug(f'Found deployment match for {deployment_id}')
×
789
            if not isinstance(timestamp_date, (str, int)):
×
790
                if deployment.start_date > timestamp_date:
×
791
                    deployment.start_date = timestamp_date
×
792
                    self._registry_updates.append(deployment)
×
793
                    LOGGER.debug('Deployment start date updated.')
×
794
                elif (deployment.end_date and
×
795
                        deployment.end_date < timestamp_date):
796
                    deployment.end_date = timestamp_date
×
797
                    self._registry_updates.append(deployment)
×
798
                    LOGGER.debug('Deployment end date updated.')
×
799
            return True
×
800

801
    def check_instrument_name_and_model(self):
1✔
802
        """
803
        Validates the instance's Extended CSV source vile's #INSTRUMENT.Name
804
        and #INSTRUMENT.Model and returns True if no errors are found.
805

806
        Adjusts the Extended CSV contents if necessary to form a match.
807

808
        :returns: `bool` of whether the input file's instrument name and model
809
                  validated successfully.
810
        """
811

812
        success = True
×
813

814
        name = self.extcsv.extcsv['INSTRUMENT']['Name']
×
815
        model = self.extcsv.extcsv['INSTRUMENT']['Model']
×
816

817
        valueline = self.extcsv.line_num('INSTRUMENT') + 2
×
818

819
        if not name or name.lower() in ['na', 'n/a']:
×
820
            if not self._add_to_report(223, valueline):
×
821
                success = False
×
822
            self.extcsv.extcsv['INSTRUMENT']['Name'] = name = 'UNKNOWN'
×
823
        if not model or str(model).lower() in ['na', 'n/a']:
×
824
            if not self._add_to_report(224, valueline):
×
825
                success = False
×
826
            self.extcsv.extcsv['INSTRUMENT']['Model'] = model = 'UNKNOWN'
×
827

828
        if not success:
×
829
            return False
×
830

831
        LOGGER.debug('Casting name and model to string for further checking')
×
832
        name = str(name)
×
833
        model = str(model)
×
834

835
        # Check data registry for matching instrument name
836
        instrument = self.registry.query_by_field(Instrument, 'name', name,
×
837
                                                  case_insensitive=True)
838
        if instrument:
×
839
            name = instrument.name
×
840
            self.extcsv.extcsv['INSTRUMENT']['Name'] = instrument.name
×
841
        elif not self._add_to_report(331, valueline, name=name):
×
842
            success = False
×
843

844
        # Check data registry for matching instrument model
845
        instrument = self.registry.query_by_field(Instrument, 'model', model,
×
846
                                                  case_insensitive=True)
847
        if instrument:
×
848
            model = instrument.model
×
849
            self.extcsv.extcsv['INSTRUMENT']['Model'] = instrument.model
×
850
        elif not self._add_to_report(332, valueline):
×
851
            success = False
×
852

853
        return success
×
854

855
    def check_instrument(self):
1✔
856
        """
857
        Validates the instance's Extended CSV source file's #INSTRUMENT table
858
        and returns True if no errors are found.
859

860
        Adjusts the Extended CSV contents if necessary to form a match.
861

862
        Prerequisite: #INSTRUMENT.Name,
863
                      #INSTRUMENT.Model,
864
                      #PLATFORM.ID and
865
                      #CONTENT.Category are all trusted values.
866

867
        :returns: `bool` of whether the input file's instrument collectively
868
                  validated successfully.
869
        """
870

871
        serial = self.extcsv.extcsv['INSTRUMENT']['Number']
×
872

873
        self.extcsv.extcsv['INSTRUMENT']['Number'] = \
×
874
            serial = correct_instrument_value(serial, 'serial')
875

876
        instrument = build_instrument(self.extcsv)
×
877
        fields = ['name', 'model', 'serial',
×
878
                  'station_id', 'dataset_id', 'deployment_id']
879
        case_insensitive = ['name', 'model', 'serial']
×
880

881
        model = {field: getattr(instrument, field) for field in fields}
×
882
        response = self.registry.query_multiple_fields(
×
883
            Instrument, model, fields, case_insensitive)
884

885
        if not response:
×
886
            LOGGER.warning(f'No instrument {instrument.instrument_id} found in registry')  # noqa
×
887
            return False
×
888
        else:
889
            LOGGER.debug(f'Found instrument match for {instrument.instrument_id}')  # noqa
×
890

891
            self.extcsv.extcsv['INSTRUMENT']['Number'] = response.serial
×
892
            return True
×
893

894
    def check_location(self):
1✔
895
        """
896
        Validates the instance's Extended CSV source file's #LOCATION table
897
        against the location of the instrument from the file, and returns
898
        True if no errors are found.
899

900
        :returns: `bool` of whether the input file's location
901
                  validated successfully.
902
        """
903

904
        success = True
×
905

906
        instrument_id = build_instrument(self.extcsv).instrument_id
×
907

908
        process_config = config.EXTRAS['processing']
×
909

910
        lat = self.extcsv.extcsv['LOCATION']['Latitude']
×
911
        lon = self.extcsv.extcsv['LOCATION']['Longitude']
×
912
        height = self.extcsv.extcsv['LOCATION'].get('Height')
×
913
        valueline = self.extcsv.line_num('LOCATION') + 2
×
914

915
        try:
×
916
            lat_numeric = float(lat)
×
917
            if -90 <= lat_numeric <= 90:
×
918
                LOGGER.debug('Validated instrument latitude')
×
919
            elif not self._add_to_report(325, valueline, field='Latitude',
×
920
                                         lower=-90, upper=90):
921
                success = False
×
922
        except ValueError:
×
923
            if not self._add_to_report(339, valueline, field='Longitude'):
×
924
                success = False
×
925

926
            self.extcsv.extcsv['LOCATION']['Latitude'] = lat = None
×
927
            lat_numeric = None
×
928

929
        try:
×
930
            lon_numeric = float(lon)
×
931
            if -180 <= lon_numeric <= 180:
×
932
                LOGGER.debug('Validated instrument longitude')
×
933
            elif not self._add_to_report(325, valueline, field='Longitude',
×
934
                                         lower=-180, upper=180):
935
                success = False
×
936
        except ValueError:
×
937
            if not self._add_to_report(339, valueline, field='Longitude'):
×
938
                success = False
×
939

940
            self.extcsv.extcsv['LOCATION']['Longitude'] = lon = None
×
941
            lon_numeric = None
×
942

943
        try:
×
944
            height_numeric = float(height) if height else None
×
945
            if not height or -50 <= height_numeric <= 5100:
×
946
                LOGGER.debug('Validated instrument height')
×
947
            elif not self._add_to_report(
×
948
                    326, valueline, lower=-50, upper=5100):
949
                success = False
×
950
        except ValueError:
×
951
            if not self._add_to_report(324, valueline):
×
952
                success = False
×
953

954
            self.extcsv.extcsv['LOCATION']['Height'] = height = None
×
955
            height_numeric = None
×
956

957
        station_type = self.extcsv.extcsv['PLATFORM'].get('Type', 'STN')
×
958
        ignore_ships = not process_config['ships_ignore_location']
×
959

960
        if not success:
×
961
            return False
×
962
        elif station_type == 'SHP' and ignore_ships:
×
963
            LOGGER.debug('Not validating shipboard instrument location')
×
964
            return True
×
965
        elif instrument_id is not None:
×
966
            instrument = self.registry.query_by_field(Instrument,
×
967
                                                      'instrument_id',
968
                                                      instrument_id)
969
            if not instrument:
×
970
                return True
×
971

972
            lat_interval = process_config['latitude_error_distance']
×
973
            lon_interval = process_config['longitude_error_distance']
×
974
            height_interval = process_config['height_error_distance']
×
975

976
            polar_latitude_range = process_config['polar_latitude_range']
×
977
            ignore_polar_lon = process_config['polar_ignore_longitude']
×
978

979
            in_polar_region = lat_numeric is not None \
×
980
                and abs(lat_numeric) > 90 - polar_latitude_range
981

982
            if lat_numeric is not None and instrument.y is not None \
×
983
               and abs(lat_numeric - instrument.y) >= lat_interval:
984
                if not self._add_to_report(327, valueline, field='Latitude'):
×
985
                    success = False
×
986
            if lon_numeric is not None and instrument.x is not None:
×
987
                if in_polar_region and ignore_polar_lon:
×
988
                    LOGGER.info('Skipping longitude check in polar region')
×
989
                elif abs(lon_numeric - instrument.x) >= lon_interval:
×
990
                    if not self._add_to_report(327, valueline,
×
991
                                               field='Longitude'):
992
                        success = False
×
993
            if height_numeric is not None and instrument.z is not None \
×
994
               and abs(height_numeric - instrument.z) >= height_interval:
995
                if not self._add_to_report(328, valueline):
×
996
                    success = False
×
997

998
        return success
×
999

1000
    def check_content(self):
1✔
1001
        """
1002
        Validates the instance's Extended CSV source file's #CONTENT.Level
1003
        and #CONTENT.Form by comparing them to other tables. Returns
1004
        True if no errors were encountered.
1005

1006
        Fill is the Extended CSV with missing values if possible.
1007

1008
        Prerequisite: #CONTENT.Category is a trusted value.
1009

1010
        :returns: `bool` of whether the input file's #CONTENT table
1011
                  collectively validated successfully.
1012
        """
1013

1014
        success = True
×
1015

1016
        dataset_name = self.extcsv.extcsv['CONTENT']['Category']
×
1017
        dataset_level = self.extcsv.extcsv['CONTENT']['Level']
×
1018
        dataset_form = self.extcsv.extcsv['CONTENT']['Form']
×
1019

1020
        valueline = self.extcsv.line_num('CONTENT') + 2
×
1021

1022
        if not dataset_level:
×
1023
            if (
×
1024
                dataset_name == 'UmkehrN14'
1025
                and 'C_PROFILE' in self.extcsv.extcsv
1026
            ):
1027
                if not self._add_to_report(217, valueline, value=2.0):
×
1028
                    success = False
×
1029
                self.extcsv.extcsv['CONTENT']['Level'] = dataset_level = 2.0
×
1030
            else:
1031
                if not self._add_to_report(217, valueline, value=1.0):
×
1032
                    success = False
×
1033
                self.extcsv.extcsv['CONTENT']['Level'] = dataset_level = 1.0
×
1034
        elif not isinstance(dataset_level, float):
×
1035
            try:
×
1036
                if not self._add_to_report(
×
1037
                    218, valueline, oldvalue=dataset_level,
1038
                    newvalue=float(dataset_level),
1039
                ):
1040
                    success = False
×
1041
                self.extcsv.extcsv['CONTENT']['Level'] = dataset_level = float(
×
1042
                    dataset_level
1043
                )
1044
            except ValueError:
×
1045
                if not self._add_to_report(310, valueline):
×
1046
                    success = False
×
1047

1048
        if dataset_name in [
×
1049
            'UmkehrN14_1.0', 'UmkehrN14_2.0'
1050
        ]:
1051
            table_index = 'UmkehrN14'
×
1052
        else:
1053
            table_index = dataset_name
×
1054

1055
        if str(dataset_level) not in DOMAINS['Datasets'][table_index]:
×
1056
            if not self._add_to_report(
×
1057
                309, valueline, dataset=dataset_name
1058
            ):
1059
                success = False
×
1060

1061
        if not isinstance(dataset_form, int):
×
1062
            try:
×
1063
                if not self._add_to_report(
×
1064
                    219, valueline, oldvalue=dataset_form,
1065
                    newvalue=int(dataset_form),
1066
                ):
1067
                    success = False
×
1068
                self.extcsv.extcsv['CONTENT']['Form'] = dataset_form = int(
×
1069
                    dataset_form
1070
                )
1071
            except ValueError:
×
1072
                if not self._add_to_report(311, valueline):
×
1073
                    success = False
×
1074

1075
        return success
×
1076

1077
    def check_data_generation(self):
1✔
1078
        """
1079
        Validates the instance's Extended CSV source file's
1080
        #DATA_GENERATION.Date and #DATA_GENERATION.Version by comparison
1081
        with other tables. Returns True if no errors were encountered.
1082

1083
        Fill in the Extended CSV with missing values if possible.
1084

1085
        :returns: `bool` of whether the input file's #DATA_GENERATION table
1086
                  collectively validated successfully.
1087
        """
1088

1089
        success = True
×
1090

1091
        dg_date = self.extcsv.extcsv['DATA_GENERATION'].get('Date')
×
1092
        version = self.extcsv.extcsv['DATA_GENERATION'].get('Version')
×
1093

1094
        valueline = self.extcsv.line_num('DATA_GENERATION')
×
1095

1096
        if not dg_date:
×
1097
            if not self._add_to_report(221, valueline):
×
1098
                success = False
×
1099

1100
            kwargs = {key: getattr(self.process_start, key)
×
1101
                      for key in ['year', 'month', 'day']}
1102
            today_date = datetime(**kwargs)
×
1103

1104
            self.extcsv.extcsv['DATA_GENERATION']['Date'] = today_date
×
1105
            dg_date = today_date
×
1106

1107
        try:
×
1108
            numeric_version = float(version)
×
1109
        except TypeError:
×
1110
            if not self._add_to_report(314, valueline, default=1.0):
×
1111
                success = False
×
1112

1113
            self.extcsv.extcsv['DATA_GENERATION']['Version'] = version = '1.0'
×
1114
            numeric_version = 1.0
×
1115
        except ValueError:
×
1116
            try:
×
1117
                while version.count('.') > 1 and version.endswith('.0'):
×
1118
                    version = version[:-2]
×
1119
                numeric_version = float(version)
×
1120
            except ValueError:
×
1121
                if not self._add_to_report(316, valueline):
×
1122
                    success = False
×
1123

1124
        if not success:
×
1125
            return False
×
1126

1127
        if not 0 <= numeric_version <= 20:
×
1128
            if not self._add_to_report(315, valueline, lower=0.0, upper=20.0):
×
1129
                success = False
×
1130
        if str(version) == str(int(numeric_version)):
×
1131
            if not self._add_to_report(222, valueline):
×
1132
                success = False
×
1133

1134
            self.extcsv.extcsv['DATA_GENERATION']['Version'] = \
×
1135
                numeric_version
1136

1137
        return success
×
1138

1139
    def check_time_series(self):
1✔
1140
        """
1141
        Validate the input Extended CSV source file's dates across all tables
1142
        to ensure that no date is more recent that #DATA_GENERATION.Date.
1143

1144
        :returns: `bool` of whether the input file's time fields collectively
1145
                  validated successfully.
1146
        """
1147

1148
        success = True
×
1149

1150
        dg_date = self.extcsv.extcsv['DATA_GENERATION']['Date']
×
1151
        ts_time = self.extcsv.extcsv['TIMESTAMP'].get('Time')
×
1152

1153
        for table, body in self.extcsv.extcsv.items():
×
1154
            if table == 'DATA_GENERATION':
×
1155
                continue
×
1156

1157
            valueline = self.extcsv.line_num(table) + 2
×
1158

1159
            date_column = body.get('Date', [])
×
1160
            if not isinstance(date_column, list):
×
1161
                date_column = [date_column]
×
1162

1163
            for line, other_date in enumerate(date_column, valueline):
×
1164
                if (isinstance(other_date, (str, int, type(None)))
×
1165
                   or isinstance(dg_date, (str, int, type(None)))):
1166
                    err_code = 336 if table.startswith('TIMESTAMP') else 337
×
1167
                    if not self._add_to_report(err_code, line, table=table):
×
1168
                        success = False
×
1169
                else:
1170
                    if other_date > dg_date:
×
1171
                        err_code = (
×
1172
                            336 if table.startswith('TIMESTAMP')
1173
                            else 337
1174
                        )
1175
                        if not self._add_to_report(err_code,
×
1176
                                                   line, table=table):
1177
                            success = False
×
1178

1179
            time_column = body.get('Time', [])
×
1180
            if not isinstance(time_column, list):
×
1181
                time_column = [time_column]
×
1182

1183
            if ts_time:
×
1184
                for line, other_time in enumerate(time_column, valueline):
×
1185
                    if (isinstance(other_time, (str, int, type(None)))
×
1186
                            or isinstance(ts_time, (str, int, type(None)))):
1187
                        pass
×
1188
                    elif other_time and other_time < ts_time:
×
1189
                        if not self._add_to_report(228, line):
×
1190
                            success = False
×
1191

1192
        return success
×
1193

1194
    def check_data_record(self, data_record):
1✔
1195
        """
1196
        Validate the data record made from the input Extended CSV file,
1197
        and look for collisions with any previous submissions of the
1198
        same data.
1199

1200
        Prerequisite: #DATA_GENERATION.Date,
1201
                      #DATA_GENERATION.Version,
1202
                      #INSTRUMENT.Name,
1203
                      and #INSTRUMENT.Number are all trusted values
1204
                      and self.data_record exists.
1205

1206
        :returns: `bool` of whether the data record metadata validated
1207
                  successfully.
1208
        """
1209

1210
        success = True
×
1211

1212
        dg_date = self.extcsv.extcsv['DATA_GENERATION']['Date']
×
1213
        version = self.extcsv.extcsv['DATA_GENERATION']['Version']
×
1214
        dg_valueline = self.extcsv.line_num('DATA_GENERATION') + 2
×
1215

1216
        id_components = data_record.data_record_id.split(':')
×
1217
        id_components[-1] = r'.*\..*'  # Pattern for floating-point number
×
1218
        identifier_pattern = ':'.join(id_components)
×
1219

1220
        LOGGER.debug('Verifying if URN already exists')
×
1221

1222
        response = self.registry.query_by_pattern(
×
1223
            DataRecord, 'data_record_id', identifier_pattern)
1224

1225
        if not response:
×
1226
            return True
×
1227

1228
        old_dg_date = response.data_generation_date
×
1229
        # Set placeholder version number
1230
        old_version = 0.0
×
1231
        try:
×
1232
            old_version = float(response.data_generation_version)
×
1233
        except ValueError:
×
1234
            success = False
×
1235
            return success
×
1236

1237
        dg_date_equal = dg_date == old_dg_date
×
1238
        dg_date_before = dg_date < old_dg_date
×
1239
        version_equal = version == old_version
×
1240

1241
        if dg_date_before:
×
1242
            if not self._add_to_report(229, dg_valueline):
×
1243
                success = False
×
1244
        elif dg_date_equal and version_equal:
×
1245
            if not self._add_to_report(401, dg_valueline):
×
1246
                success = False
×
1247
        elif dg_date_equal:
×
1248
            if not self._add_to_report(402, dg_valueline):
×
1249
                success = False
×
1250
        elif version_equal:
×
1251
            if not self._add_to_report(403, dg_valueline):
×
1252
                success = False
×
1253

1254
        instrument_name = self.extcsv.extcsv['INSTRUMENT']['Name']
×
1255
        instrument_serial = self.extcsv.extcsv['INSTRUMENT']['Number']
×
1256
        old_serial = response.instrument.serial
×
1257

1258
        if instrument_name == 'ECC' and instrument_serial != old_serial:
×
1259
            instrument_valueline = self.extcsv.line_num('INSTRUMENT') + 2
×
1260
            if not self._add_to_report(404, instrument_valueline):
×
1261
                success = False
×
1262

1263
        return success
×
1264

1265

1266
class ProcessingError(Exception):
1✔
1267
    """custom exception handler"""
1268
    pass
1✔
1269

1270

1271
def correct_instrument_value(instrument_value, value_type):
1✔
1272
    """
1273
    Corrects the values for the instrument table if not already in the correct
1274
    format.
1275

1276
    Prerequisite: #INSTRUMENT.Number and/or #INSTRUMENT.Model
1277

1278
    :param instrument_value: The value of the instrument (serial number or
1279
    model).
1280
    :param value_type: The type of value ('serial' or other).
1281
    :return: Corrected instrument value.
1282
    """
1283
    # Convert to string to handle non-string inputs (e.g., numbers)
1284
    instrument_value = str(instrument_value).strip()
×
1285

NEW
1286
    if not instrument_value or instrument_value.lower() in ['na', 'n/a']:
×
NEW
1287
        return 'UNKNOWN'
×
1288

NEW
1289
    if instrument_value.lower() == 'unknown':
×
NEW
1290
        return 'UNKNOWN'
×
1291

NEW
1292
    if value_type in ('model', 'name'):
×
NEW
1293
        return instrument_value.capitalize()
×
1294

NEW
1295
    if value_type == 'serial':
×
NEW
1296
        instrument_value = instrument_value.lstrip('0') or '0'
×
1297

1298
    return instrument_value
×
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