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

jmathai / elodie / 5a4a5a1b-cd18-4513-8aa9-9419113e7a04

pending completion
5a4a5a1b-cd18-4513-8aa9-9419113e7a04

Pull #450

circleci

GitHub
New option for file name capitalization handling
Pull Request #450: New option for file name capitalization handling

2 of 2 new or added lines in 1 file covered. (100.0%)

1294 of 1429 relevant lines covered (90.55%)

0.91 hits per line

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

93.07
/elodie/filesystem.py
1
"""
2
General file system methods.
3

4
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
5
"""
6
from __future__ import print_function
1✔
7
from builtins import object
1✔
8

9
import os
1✔
10
import re
1✔
11
import shutil
1✔
12
import time
1✔
13

14
from elodie import compatability
1✔
15
from elodie import geolocation
1✔
16
from elodie import log
1✔
17
from elodie.config import load_config
1✔
18
from elodie.localstorage import Db
1✔
19
from elodie.media.base import Base, get_all_subclasses
1✔
20
from elodie.plugins.plugins import Plugins
1✔
21

22
class FileSystem(object):
1✔
23
    """A class for interacting with the file system."""
24

25
    def __init__(self):
1✔
26
        # The default folder path is along the lines of 2017-06-17_01-04-14-dsc_1234-some-title.jpg
27
        self.default_file_name_definition = {
1✔
28
            'date': '%Y-%m-%d_%H-%M-%S',
29
            'name': '%date-%original_name-%title.%extension',
30
        }
31
        # The default folder path is along the lines of 2015-01-Jan/Chicago
32
        self.default_folder_path_definition = {
1✔
33
            'date': '%Y-%m-%b',
34
            'location': '%city',
35
            'full_path': '%date/%album|%location|"{}"'.format(
36
                            geolocation.__DEFAULT_LOCATION__
37
                         ),
38
        }
39
        self.cached_file_name_definition = None
1✔
40
        self.cached_folder_path_definition = None
1✔
41
        # Python3 treats the regex \s differently than Python2.
42
        # It captures some additional characters like the unicode checkmark \u2713.
43
        # See build failures in Python3 here.
44
        #  https://travis-ci.org/jmathai/elodie/builds/483012902
45
        self.whitespace_regex = '[ \t\n\r\f\v]+'
1✔
46

47
        # Instantiate a plugins object
48
        self.plugins = Plugins()
1✔
49

50
    def create_directory(self, directory_path):
1✔
51
        """Create a directory if it does not already exist.
52

53
        :param str directory_name: A fully qualified path of the
54
            to create.
55
        :returns: bool
56
        """
57
        try:
1✔
58
            if os.path.exists(directory_path):
1✔
59
                return True
1✔
60
            else:
61
                os.makedirs(directory_path)
1✔
62
                return True
1✔
63
        except OSError:
1✔
64
            # OSError is thrown for cases like no permission
65
            pass
1✔
66

67
        return False
1✔
68

69
    def delete_directory_if_empty(self, directory_path):
1✔
70
        """Delete a directory only if it's empty.
71

72
        Instead of checking first using `len([name for name in
73
        os.listdir(directory_path)]) == 0`, we catch the OSError exception.
74

75
        :param str directory_name: A fully qualified path of the directory
76
            to delete.
77
        """
78
        try:
1✔
79
            os.rmdir(directory_path)
1✔
80
            return True
1✔
81
        except OSError:
1✔
82
            pass
1✔
83

84
        return False
1✔
85

86
    def get_all_files(self, path, extensions=None, exclude_regex_list=set()):
1✔
87
        """Recursively get all files which match a path and extension.
88

89
        :param str path string: Path to start recursive file listing
90
        :param tuple(str) extensions: File extensions to include (whitelist)
91
        :returns: generator
92
        """
93
        # If extensions is None then we get all supported extensions
94
        if not extensions:
1✔
95
            extensions = set()
1✔
96
            subclasses = get_all_subclasses(Base)
1✔
97
            for cls in subclasses:
1✔
98
                extensions.update(cls.extensions)
1✔
99

100
        # Create a list of compiled regular expressions to match against the file path
101
        compiled_regex_list = [re.compile(regex) for regex in exclude_regex_list]
1✔
102
        for dirname, dirnames, filenames in os.walk(path):
1✔
103
            for filename in filenames:
1✔
104
                # If file extension is in `extensions` 
105
                # And if file path is not in exclude regexes
106
                # Then append to the list
107
                filename_path = os.path.join(dirname, filename)
1✔
108
                if (
1✔
109
                        os.path.splitext(filename)[1][1:].lower() in extensions and
110
                        not self.should_exclude(filename_path, compiled_regex_list, False)
111
                    ):
112
                    yield filename_path
1✔
113

114
    def get_current_directory(self):
1✔
115
        """Get the current working directory.
116

117
        :returns: str
118
        """
119
        return os.getcwd()
1✔
120

121
    def get_file_name(self, metadata):
1✔
122
        """Generate file name for a photo or video using its metadata.
123

124
        Originally we hardcoded the file name to include an ISO date format.
125
        We use an ISO8601-like format for the file name prefix. Instead of
126
        colons as the separator for hours, minutes and seconds we use a hyphen.
127
        https://en.wikipedia.org/wiki/ISO_8601#General_principles
128

129
        PR #225 made the file name customizable and fixed issues #107 #110 #111.
130
        https://github.com/jmathai/elodie/pull/225
131

132
        :param media: A Photo or Video instance
133
        :type media: :class:`~elodie.media.photo.Photo` or
134
            :class:`~elodie.media.video.Video`
135
        :returns: str or None for non-photo or non-videos
136
        """
137
        if(metadata is None):
1✔
138
            return None
×
139

140
        # Get the name template and definition.
141
        # Name template is in the form %date-%original_name-%title.%extension
142
        # Definition is in the form
143
        #  [
144
        #    [('date', '%Y-%m-%d_%H-%M-%S')],
145
        #    [('original_name', '')], [('title', '')], // contains a fallback
146
        #    [('extension', '')]
147
        #  ]
148
        name_template, definition = self.get_file_name_definition()
1✔
149

150
        name = name_template
1✔
151
        for parts in definition:
1✔
152
            this_value = None
1✔
153
            for this_part in parts:
1✔
154
                part, mask = this_part
1✔
155
                if part in ('date', 'day', 'month', 'year'):
1✔
156
                    this_value = time.strftime(mask, metadata['date_taken'])
1✔
157
                    break
1✔
158
                elif part in ('location', 'city', 'state', 'country'):
1✔
159
                    place_name = geolocation.place_name(
×
160
                        metadata['latitude'],
161
                        metadata['longitude']
162
                    )
163

164
                    location_parts = re.findall('(%[^%]+)', mask)
×
165
                    this_value = self.parse_mask_for_location(
×
166
                        mask,
167
                        location_parts,
168
                        place_name,
169
                    )
170
                    break
×
171
                elif part in ('album', 'extension', 'title'):
1✔
172
                    if metadata[part]:
1✔
173
                        this_value = re.sub(self.whitespace_regex, '-', metadata[part].strip())
1✔
174
                        break
1✔
175
                elif part in ('original_name'):
1✔
176
                    # First we check if we have metadata['original_name'].
177
                    # We have to do this for backwards compatibility because
178
                    #   we original did not store this back into EXIF.
179
                    if metadata[part]:
1✔
180
                        this_value = os.path.splitext(metadata['original_name'])[0]
1✔
181
                    else:
182
                        # We didn't always store original_name so this is 
183
                        #  for backwards compatability.
184
                        # We want to remove the hardcoded date prefix we used 
185
                        #  to add to the name.
186
                        # This helps when re-running the program on file 
187
                        #  which were already processed.
188
                        this_value = re.sub(
1✔
189
                            '^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-',
190
                            '',
191
                            metadata['base_name']
192
                        )
193
                        if(len(this_value) == 0):
1✔
194
                            this_value = metadata['base_name']
×
195

196
                    # Lastly we want to sanitize the name
197
                    this_value = re.sub(self.whitespace_regex, '-', this_value.strip())
1✔
198
                elif part.startswith('"') and part.endswith('"'):
×
199
                    this_value = part[1:-1]
×
200
                    break
×
201

202
            # Here we replace the placeholder with it's corresponding value.
203
            # Check if this_value was not set so that the placeholder
204
            #  can be removed completely.
205
            # For example, %title- will be replaced with ''
206
            # Else replace the placeholder (i.e. %title) with the value.
207
            if this_value is None:
1✔
208
                name = re.sub(
1✔
209
                    #'[^a-z_]+%{}'.format(part),
210
                    '[^a-zA-Z0-9_]+%{}'.format(part),
211
                    '',
212
                    name,
213
                )
214
            else:
215
                name = re.sub(
1✔
216
                    '%{}'.format(part),
217
                    this_value,
218
                    name,
219
                )
220

221
        config = load_config()
1✔
222

223
        if('File' in config and 'capitalization' in config['File'] and config['File']['capitalization'] == 'upper'):
1✔
224
            return name.upper()
1✔
225
        elif('File' in config and 'capitalization' in config['File'] and config['File']['capitalization'] == 'title'):
1✔
226
            # Mine is:
227
            # date=%d-%H%M%S
228
            # name=%date [%city] %original_name.%extension
229
            # Since strings before "]" are quite stable, it ensures that they can be split properly by "]" no matter what the original name is.
230
            # "]" is replaceable.
231
            return name.split(']', maxsplit=1)[0].title() + ']' + name.split(']', maxsplit=1)[1]
×
232
        else:
233
            return name.lower()
1✔
234

235
    def get_file_name_definition(self):
1✔
236
        """Returns a list of folder definitions.
237

238
        Each element in the list represents a folder.
239
        Fallback folders are supported and are nested lists.
240
        Return values take the following form.
241
        [
242
            ('date', '%Y-%m-%d'),
243
            [
244
                ('location', '%city'),
245
                ('album', ''),
246
                ('"Unknown Location", '')
247
            ]
248
        ]
249

250
        :returns: list
251
        """
252
        # If we've done this already then return it immediately without
253
        # incurring any extra work
254
        if self.cached_file_name_definition is not None:
1✔
255
            return self.cached_file_name_definition
1✔
256

257
        config = load_config()
1✔
258

259
        # If File is in the config we assume name and its
260
        #  corresponding values are also present
261
        config_file = self.default_file_name_definition
1✔
262
        if('File' in config):
1✔
263
            config_file = config['File']
1✔
264

265
        # Find all subpatterns of name that map to the components of the file's
266
        #  name.
267
        #  I.e. %date-%original_name-%title.%extension => ['date', 'original_name', 'title', 'extension'] #noqa
268
        path_parts = re.findall(
1✔
269
                         '(\%[a-z_]+)',
270
                         config_file['name']
271
                     )
272

273
        if not path_parts or len(path_parts) == 0:
1✔
274
            return (config_file['name'], self.default_file_name_definition)
×
275

276
        self.cached_file_name_definition = []
1✔
277
        for part in path_parts:
1✔
278
            if part in config_file:
1✔
279
                part = part[1:]
×
280
                self.cached_file_name_definition.append(
×
281
                    [(part, config_file[part])]
282
                )
283
            else:
284
                this_part = []
1✔
285
                for p in part.split('|'):
1✔
286
                    p = p[1:]
1✔
287
                    this_part.append(
1✔
288
                        (p, config_file[p] if p in config_file else '')
289
                    )
290
                self.cached_file_name_definition.append(this_part)
1✔
291

292
        self.cached_file_name_definition = (config_file['name'], self.cached_file_name_definition)
1✔
293
        return self.cached_file_name_definition
1✔
294

295
    def get_folder_path_definition(self):
1✔
296
        """Returns a list of folder definitions.
297

298
        Each element in the list represents a folder.
299
        Fallback folders are supported and are nested lists.
300
        Return values take the following form.
301
        [
302
            ('date', '%Y-%m-%d'),
303
            [
304
                ('location', '%city'),
305
                ('album', ''),
306
                ('"Unknown Location", '')
307
            ]
308
        ]
309

310
        :returns: list
311
        """
312
        # If we've done this already then return it immediately without
313
        # incurring any extra work
314
        if self.cached_folder_path_definition is not None:
1✔
315
            return self.cached_folder_path_definition
1✔
316

317
        config = load_config()
1✔
318

319
        # If Directory is in the config we assume full_path and its
320
        #  corresponding values (date, location) are also present
321
        config_directory = self.default_folder_path_definition
1✔
322
        if('Directory' in config):
1✔
323
            config_directory = config['Directory']
1✔
324

325
        # Find all subpatterns of full_path that map to directories.
326
        #  I.e. %foo/%bar => ['foo', 'bar']
327
        #  I.e. %foo/%bar|%example|"something" => ['foo', 'bar|example|"something"']
328
        path_parts = re.findall(
1✔
329
                         '(\%[^/]+)',
330
                         config_directory['full_path']
331
                     )
332

333
        if not path_parts or len(path_parts) == 0:
1✔
334
            return self.default_folder_path_definition
×
335

336
        self.cached_folder_path_definition = []
1✔
337
        for part in path_parts:
1✔
338
            part = part.replace('%', '')
1✔
339
            if part in config_directory:
1✔
340
                self.cached_folder_path_definition.append(
1✔
341
                    [(part, config_directory[part])]
342
                )
343
            else:
344
                this_part = []
1✔
345
                for p in part.split('|'):
1✔
346
                    this_part.append(
1✔
347
                        (p, config_directory[p] if p in config_directory else '')
348
                    )
349
                self.cached_folder_path_definition.append(this_part)
1✔
350

351
        return self.cached_folder_path_definition
1✔
352

353
    def get_folder_path(self, metadata, path_parts=None):
1✔
354
        """Given a media's metadata this function returns the folder path as a string.
355

356
        :param dict metadata: Metadata dictionary.
357
        :returns: str
358
        """
359
        if path_parts is None:
1✔
360
            path_parts = self.get_folder_path_definition()
1✔
361
        path = []
1✔
362
        for path_part in path_parts:
1✔
363
            # We support fallback values so that
364
            #  'album|city|"Unknown Location"
365
            #  %album|%city|"Unknown Location" results in
366
            #  My Album - when an album exists
367
            #  Sunnyvale - when no album exists but a city exists
368
            #  Unknown Location - when neither an album nor location exist
369
            for this_part in path_part:
1✔
370
                part, mask = this_part
1✔
371
                this_path = self.get_dynamic_path(part, mask, metadata)
1✔
372
                if this_path:
1✔
373
                    path.append(this_path.strip())
1✔
374
                    # We break as soon as we have a value to append
375
                    # Else we continue for fallbacks
376
                    break
1✔
377
        return os.path.join(*path)
1✔
378

379
    def get_dynamic_path(self, part, mask, metadata):
1✔
380
        """Parse a specific folder's name given a mask and metadata.
381

382
        :param part: Name of the part as defined in the path (i.e. date from %date)
383
        :param mask: Mask representing the template for the path (i.e. %city %state
384
        :param metadata: Metadata dictionary.
385
        :returns: str
386
        """
387

388
        # Each part has its own custom logic and we evaluate a single part and return
389
        #  the evaluated string.
390
        if part in ('custom'):
1✔
391
            custom_parts = re.findall('(%[a-z_]+)', mask)
1✔
392
            folder = mask
1✔
393
            for i in custom_parts:
1✔
394
                folder = folder.replace(
1✔
395
                    i,
396
                    self.get_dynamic_path(i[1:], i, metadata)
397
                )
398
            return folder
1✔
399
        elif part in ('date'):
1✔
400
            config = load_config()
1✔
401
            # If Directory is in the config we assume full_path and its
402
            #  corresponding values (date, location) are also present
403
            config_directory = self.default_folder_path_definition
1✔
404
            if('Directory' in config):
1✔
405
                config_directory = config['Directory']
1✔
406
            date_mask = ''
1✔
407
            if 'date' in config_directory:
1✔
408
                date_mask = config_directory['date']
1✔
409
            return time.strftime(date_mask, metadata['date_taken'])
1✔
410
        elif part in ('day', 'month', 'year'):
1✔
411
            return time.strftime(mask, metadata['date_taken'])
1✔
412
        elif part in ('location', 'city', 'state', 'country'):
1✔
413
            place_name = geolocation.place_name(
1✔
414
                metadata['latitude'],
415
                metadata['longitude']
416
            )
417

418
            location_parts = re.findall('(%[^%]+)', mask)
1✔
419
            parsed_folder_name = self.parse_mask_for_location(
1✔
420
                mask,
421
                location_parts,
422
                place_name,
423
            )
424
            return parsed_folder_name
1✔
425
        elif part in ('album', 'camera_make', 'camera_model'):
1✔
426
            if metadata[part]:
1✔
427
                return metadata[part]
1✔
428
        elif part.startswith('"') and part.endswith('"'):
1✔
429
            # Fallback string
430
            return part[1:-1]
1✔
431

432
        return ''
1✔
433

434
    def parse_mask_for_location(self, mask, location_parts, place_name):
1✔
435
        """Takes a mask for a location and interpolates the actual place names.
436

437
        Given these parameters here are the outputs.
438

439
        mask=%city
440
        location_parts=[('%city','%city','city')]
441
        place_name={'city': u'Sunnyvale'}
442
        output=Sunnyvale
443

444
        mask=%city-%state
445
        location_parts=[('%city-','%city','city'), ('%state','%state','state')]
446
        place_name={'city': u'Sunnyvale', 'state': u'California'}
447
        output=Sunnyvale-California
448

449
        mask=%country
450
        location_parts=[('%country','%country','country')]
451
        place_name={'default': u'Sunnyvale', 'city': u'Sunnyvale'}
452
        output=Sunnyvale
453

454

455
        :param str mask: The location mask in the form of %city-%state, etc
456
        :param list location_parts: A list of tuples in the form of
457
            [('%city-', '%city', 'city'), ('%state', '%state', 'state')]
458
        :param dict place_name: A dictionary of place keywords and names like
459
            {'default': u'California', 'state': u'California'}
460
        :returns: str
461
        """
462
        found = False
1✔
463
        folder_name = mask
1✔
464
        for loc_part in location_parts:
1✔
465
            # We assume the search returns a tuple of length 2.
466
            # If not then it's a bad mask in config.ini.
467
            # loc_part = '%country-random'
468
            # component_full = '%country-random'
469
            # component = '%country'
470
            # key = 'country
471
            component_full, component, key = re.search(
1✔
472
                '((%([a-z]+))[^%]*)',
473
                loc_part
474
            ).groups()
475

476
            if(key in place_name):
1✔
477
                found = True
1✔
478
                replace_target = component
1✔
479
                replace_with = place_name[key]
1✔
480
            else:
481
                replace_target = component_full
1✔
482
                replace_with = ''
1✔
483

484
            folder_name = folder_name.replace(
1✔
485
                replace_target,
486
                replace_with,
487
            )
488

489
        if(not found and folder_name == ''):
1✔
490
            folder_name = place_name['default']
1✔
491

492
        return folder_name
1✔
493

494
    def process_checksum(self, _file, allow_duplicate):
1✔
495
        db = Db()
1✔
496
        checksum = db.checksum(_file)
1✔
497
        if(checksum is None):
1✔
498
            log.info('Could not get checksum for %s.' % _file)
×
499
            return None
×
500

501
        # If duplicates are not allowed then we check if we've seen this file
502
        #  before via checksum. We also check that the file exists at the
503
        #   location we believe it to be.
504
        # If we find a checksum match but the file doesn't exist where we
505
        #  believe it to be then we write a debug log and proceed to import.
506
        checksum_file = db.get_hash(checksum)
1✔
507
        if(allow_duplicate is False and checksum_file is not None):
1✔
508
            if(os.path.isfile(checksum_file)):
1✔
509
                log.info('%s already at %s.' % (
1✔
510
                    _file,
511
                    checksum_file
512
                ))
513
                return None
1✔
514
            else:
515
                log.info('%s matched checksum but file not found at %s.' % (  # noqa
1✔
516
                    _file,
517
                    checksum_file
518
                ))
519
        return checksum
1✔
520

521
    def process_file(self, _file, destination, media, **kwargs):
1✔
522
        move = False
1✔
523
        if('move' in kwargs):
1✔
524
            move = kwargs['move']
1✔
525

526
        allow_duplicate = False
1✔
527
        if('allowDuplicate' in kwargs):
1✔
528
            allow_duplicate = kwargs['allowDuplicate']
1✔
529

530
        stat_info_original = os.stat(_file)
1✔
531
        metadata = media.get_metadata()
1✔
532

533
        if(not media.is_valid()):
1✔
534
            print('%s is not a valid media file. Skipping...' % _file)
1✔
535
            return
1✔
536

537
        checksum = self.process_checksum(_file, allow_duplicate)
1✔
538
        if(checksum is None):
1✔
539
            log.info('Original checksum returned None for %s. Skipping...' %
1✔
540
                     _file)
541
            return
1✔
542

543
        # Run `before()` for every loaded plugin and if any of them raise an exception
544
        #  then we skip importing the file and log a message.
545
        plugins_run_before_status = self.plugins.run_all_before(_file, destination)
1✔
546
        if(plugins_run_before_status == False):
1✔
547
            log.warn('At least one plugin pre-run failed for %s' % _file)
1✔
548
            return
1✔
549

550
        directory_name = self.get_folder_path(metadata)
1✔
551
        dest_directory = os.path.join(destination, directory_name)
1✔
552
        file_name = self.get_file_name(metadata)
1✔
553
        dest_path = os.path.join(dest_directory, file_name)        
1✔
554

555
        media.set_original_name()
1✔
556

557
        # If source and destination are identical then
558
        #  we should not write the file. gh-210
559
        if(_file == dest_path):
1✔
560
            print('Final source and destination path should not be identical')
1✔
561
            return
1✔
562

563
        self.create_directory(dest_directory)
1✔
564

565
        # exiftool renames the original file by appending '_original' to the
566
        # file name. A new file is written with new tags with the initial file
567
        # name. See exiftool man page for more details.
568
        exif_original_file = _file + '_original'
1✔
569

570
        # Check if the source file was processed by exiftool and an _original
571
        # file was created.
572
        exif_original_file_exists = False
1✔
573
        if(os.path.exists(exif_original_file)):
1✔
574
            exif_original_file_exists = True
1✔
575

576
        if(move is True):
1✔
577
            stat = os.stat(_file)
1✔
578
            # Move the processed file into the destination directory
579
            shutil.move(_file, dest_path)
1✔
580

581
            if(exif_original_file_exists is True):
1✔
582
                # We can remove it as we don't need the initial file.
583
                os.remove(exif_original_file)
1✔
584
            os.utime(dest_path, (stat.st_atime, stat.st_mtime))
1✔
585
        else:
586
            if(exif_original_file_exists is True):
1✔
587
                # Move the newly processed file with any updated tags to the
588
                # destination directory
589
                shutil.move(_file, dest_path)
1✔
590
                # Move the exif _original back to the initial source file
591
                shutil.move(exif_original_file, _file)
1✔
592
            else:
593
                compatability._copyfile(_file, dest_path)
×
594

595
            # Set the utime based on what the original file contained 
596
            #  before we made any changes.
597
            # Then set the utime on the destination file based on metadata.
598
            os.utime(_file, (stat_info_original.st_atime, stat_info_original.st_mtime))
1✔
599
            self.set_utime_from_metadata(metadata, dest_path)
1✔
600

601
        db = Db()
1✔
602
        db.add_hash(checksum, dest_path)
1✔
603
        db.update_hash_db()
1✔
604

605
        # Run `after()` for every loaded plugin and if any of them raise an exception
606
        #  then we skip importing the file and log a message.
607
        plugins_run_after_status = self.plugins.run_all_after(_file, destination, dest_path, metadata)
1✔
608
        if(plugins_run_after_status == False):
1✔
609
            log.warn('At least one plugin pre-run failed for %s' % _file)
×
610
            return
×
611

612

613
        return dest_path
1✔
614

615
    def set_utime_from_metadata(self, metadata, file_path):
1✔
616
        """ Set the modification time on the file based on the file name.
617
        """
618

619
        # Initialize date taken to what's returned from the metadata function.
620
        # If the folder and file name follow a time format of
621
        #   YYYY-MM-DD_HH-MM-SS-IMG_0001.JPG then we override the date_taken
622
        date_taken = metadata['date_taken']
1✔
623
        base_name = metadata['base_name']
1✔
624
        year_month_day_match = re.search(
1✔
625
            '^(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})',
626
            base_name
627
        )
628
        if(year_month_day_match is not None):
1✔
629
            (year, month, day, hour, minute, second) = year_month_day_match.groups()  # noqa
1✔
630
            date_taken = time.strptime(
1✔
631
                '{}-{}-{} {}:{}:{}'.format(year, month, day, hour, minute, second),  # noqa
632
                '%Y-%m-%d %H:%M:%S'
633
            )
634

635
            os.utime(file_path, (time.time(), time.mktime(date_taken)))
1✔
636
        else:
637
            # We don't make any assumptions about time zones and
638
            # assume local time zone.
639
            date_taken_in_seconds = time.mktime(date_taken)
1✔
640
            os.utime(file_path, (time.time(), (date_taken_in_seconds)))
1✔
641

642
    def should_exclude(self, path, regex_list=set(), needs_compiled=False):
1✔
643
        if(len(regex_list) == 0):
1✔
644
            return False
1✔
645

646
        if(needs_compiled):
1✔
647
            compiled_list = []
1✔
648
            for regex in regex_list:
1✔
649
                compiled_list.append(re.compile(regex))
1✔
650
            regex_list = compiled_list
1✔
651

652
        return any(regex.search(path) for regex in regex_list)
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

© 2023 Coveralls, Inc