Coveralls logob
Coveralls logo
  • Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IdentityPython / pyFF / 760

14 Apr 2021 - 8:28 coverage: 73.418% (-0.04%) from 73.455%
760

Pull #214

travis-ci

9181eb84f9c35729a3bad740fb7f9d93?size=18&default=identiconweb-flow
Merge 3757d7dfb into 648c5bc7e
Pull Request #214: minor fixes

19 of 23 new or added lines in 4 files covered. (82.61%)

2 existing lines in 1 file now uncovered.

2900 of 3950 relevant lines covered (73.42%)

1.47 hits per line

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

70.16
/src/pyff/utils.py
1
# coding=utf-8
2

3

4
"""
2×
5

6
This module contains various utilities.
7

8
"""
9
import base64
2×
10
import cgi
2×
11
import contextlib
2×
12
import hashlib
2×
13
import io
2×
14
import os
2×
15
import random
2×
16
import re
2×
17
import tempfile
2×
18
import threading
2×
19
import time
2×
20
import traceback
2×
21
from copy import copy
2×
22
from datetime import datetime, timedelta
2×
23
from email.utils import parsedate
2×
24
from itertools import chain
2×
25
from threading import local
2×
26
from time import gmtime, strftime
2×
27
from typing import AnyStr, Optional, Union
2×
28

29
import iso8601
2×
30
import pkg_resources
2×
31
import requests
2×
32
import xmlsec
2×
33
from _collections_abc import Mapping, MutableMapping
2×
34
from apscheduler.executors.pool import ThreadPoolExecutor
2×
35
from apscheduler.jobstores.memory import MemoryJobStore
2×
36
from apscheduler.jobstores.redis import RedisJobStore
2×
37
from apscheduler.schedulers.background import BackgroundScheduler
2×
38
from cachetools import LRUCache
2×
39
from lxml import etree
2×
40
from requests.adapters import BaseAdapter, HTTPAdapter, Response
2×
41
from requests.packages.urllib3.util.retry import Retry
2×
42
from requests.structures import CaseInsensitiveDict
2×
43
from requests_cache import CachedSession
2×
44
from requests_file import FileAdapter
2×
45
from six.moves.urllib_parse import urlparse
2×
46

47
from . import __version__
2×
48
from .constants import NS, config
2×
49
from .exceptions import *
2×
50
from .logs import get_log
2×
51

52

53
etree.set_default_parser(etree.XMLParser(resolve_entities=False))
2×
54

55
__author__ = 'leifj'
2×
56

57
log = get_log(__name__)
2×
58

59
sentinel = object()
2×
60
thread_data = local()
2×
61

62

63
def xml_error(error_log, m=None):
2×
64
    def _f(x):
2×
65
        if ":WARNING:" in x:
2×
66
            return False
2×
67
        if m is not None and m not in x:
2×
68
            return False
2×
69
        return True
2×
70

71
    return "\n".join(filter(_f, ["%s" % e for e in error_log]))
2×
72

73

74
def debug_observer(e):
2×
75
    log.error(repr(e))
!
76

77

78
def trunc_str(x, l):
2×
79
    return (x[:l] + '..') if len(x) > l else x
!
80

81

82
def resource_string(name, pfx=None):
2×
83
    """
84
    Attempt to load and return the contents (as a string) of the resource named by
85
    the first argument in the first location of:
86

87
    # as name in the current directory
88
    # as name in the `pfx` subdirectory of the current directory if provided
89
    # as name relative to the package
90
    # as pfx/name relative to the package
91

92
    The last two alternatives is used to locate resources distributed in the package.
93
    This includes certain XSLT and XSD files.
94

95
    :param name: The string name of a resource
96
    :param pfx: An optional prefix to use in searching for name
97

98
    """
99
    name = os.path.expanduser(name)
2×
100
    data = None
2×
101
    if os.path.exists(name):
2×
102
        with io.open(name) as fd:
2×
103
            data = fd.read()
2×
104
    elif pfx and os.path.exists(os.path.join(pfx, name)):
2×
105
        with io.open(os.path.join(pfx, name)) as fd:
2×
106
            data = fd.read()
2×
107
    elif pkg_resources.resource_exists(__name__, name):
2×
108
        data = pkg_resources.resource_string(__name__, name)
2×
109
    elif pfx and pkg_resources.resource_exists(__name__, "%s/%s" % (pfx, name)):
2×
110
        data = pkg_resources.resource_string(__name__, "%s/%s" % (pfx, name))
2×
111

112
    return data
2×
113

114

115
def resource_filename(name, pfx=None):
2×
116
    """
117
    Attempt to find and return the filename of the resource named by the first argument
118
    in the first location of:
119

120
    # as name in the current directory
121
    # as name in the `pfx` subdirectory of the current directory if provided
122
    # as name relative to the package
123
    # as pfx/name relative to the package
124

125
    The last two alternatives is used to locate resources distributed in the package.
126
    This includes certain XSLT and XSD files.
127

128
    :param name: The string name of a resource
129
    :param pfx: An optional prefix to use in searching for name
130

131
    """
132
    if os.path.exists(name):
2×
133
        return name
2×
134
    elif pfx and os.path.exists(os.path.join(pfx, name)):
2×
135
        return os.path.join(pfx, name)
2×
136
    elif pkg_resources.resource_exists(__name__, name):
2×
137
        return pkg_resources.resource_filename(__name__, name)
2×
138
    elif pfx and pkg_resources.resource_exists(__name__, "%s/%s" % (pfx, name)):
2×
139
        return pkg_resources.resource_filename(__name__, "%s/%s" % (pfx, name))
2×
140

141
    return None
2×
142

143

144
def totimestamp(dt, epoch=datetime(1970, 1, 1)):
2×
145
    epoch = epoch.replace(tzinfo=dt.tzinfo)
2×
146

147
    td = dt - epoch
2×
148
    ts = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 1e6
2×
149
    return int(ts)
2×
150

151

152
def dumptree(t, pretty_print=False, method='xml', xml_declaration=True):
2×
153
    """
154
    Return a string representation of the tree, optionally pretty_print(ed) (default False)
155

156
    :param t: An ElemenTree to serialize
157
    """
158
    return etree.tostring(
2×
159
        t, encoding='UTF-8', method=method, xml_declaration=xml_declaration, pretty_print=pretty_print
160
    )
161

162

163
def iso_now():
2×
164
    """
165
    Current time in ISO format
166
    """
167
    return iso_fmt()
2×
168

169

170
def iso_fmt(tstamp=None):
2×
171
    """
172
    Timestamp in ISO format
173
    """
174
    return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime(tstamp))
2×
175

176

177
def ts_now():
2×
178
    return int(time.time())
!
179

180

181
def iso2datetime(s):
2×
182
    return iso8601.parse_date(s)
2×
183

184

185
def first_text(elt, tag, default=None):
2×
186
    for matching in elt.iter(tag):
!
187
        return matching.text
!
188
    return default
!
189

190

191
class ResourceResolver(etree.Resolver):
2×
192
    def __init__(self):
2×
193
        super(ResourceResolver, self).__init__()
2×
194

195
    def resolve(self, system_url, public_id, context):
2×
196
        """
197
        Resolves URIs using the resource API
198
        """
199
        # log.debug("resolve SYSTEM URL' %s' for '%s'" % (system_url, public_id))
200
        path = system_url.split("/")
2×
201
        fn = path[len(path) - 1]
2×
202
        if pkg_resources.resource_exists(__name__, fn):
2×
203
            return self.resolve_file(pkg_resources.resource_stream(__name__, fn), context)
!
204
        elif pkg_resources.resource_exists(__name__, "schema/%s" % fn):
2×
205
            return self.resolve_file(pkg_resources.resource_stream(__name__, "schema/%s" % fn), context)
2×
206
        else:
207
            raise ValueError("Unable to locate %s" % fn)
!
208

209

210
thread_local_lock = threading.Lock()
2×
211

212

213
def schema():
2×
214
    if not hasattr(thread_data, 'schema'):
2×
215
        thread_data.schema = None
2×
216

217
    if thread_data.schema is None:
2×
218
        try:
2×
219
            thread_local_lock.acquire(blocking=True)
2×
220
            parser = etree.XMLParser()
2×
221
            parser.resolvers.add(ResourceResolver())
2×
222
            st = etree.parse(pkg_resources.resource_stream(__name__, "schema/schema.xsd"), parser)
2×
223
            thread_data.schema = etree.XMLSchema(st)
2×
224
        except etree.XMLSchemaParseError as ex:
!
225
            traceback.print_exc()
!
226
            log.error(xml_error(ex.error_log))
!
227
            raise ex
!
228
        finally:
229
            thread_local_lock.release()
2×
230
    return thread_data.schema
2×
231

232

233
def redis():
2×
234
    if not hasattr(thread_data, 'redis'):
2×
235
        thread_data.redis = None
2×
236

237
    try:
2×
238
        from redis import StrictRedis
2×
NEW
239
    except ImportError:
!
UNCOV
240
        raise ValueError("redis_py missing from dependencies")
!
241

242
    if thread_data.redis is None:
2×
243
        try:
2×
244
            thread_local_lock.acquire(blocking=True)
2×
245
            thread_data.redis = StrictRedis(host=config.redis_host, port=config.redis_port)
2×
246
        except BaseException as ex:
!
247
            traceback.print_exc()
!
248
            log.error(ex)
!
249
            raise ex
!
250
        finally:
251
            thread_local_lock.release()
2×
252

253
    return thread_data.redis
2×
254

255

256
def check_signature(t, key, only_one_signature=False):
2×
257
    if key is not None:
2×
258
        log.debug("verifying signature using %s" % key)
2×
259
        refs = xmlsec.verified(t, key, drop_signature=True)
2×
260
        if only_one_signature and len(refs) != 1:
2×
261
            raise MetadataException("XML metadata contains %d signatures - exactly 1 is required" % len(refs))
!
262
        t = refs[0]  # prevent wrapping attacks
2×
263

264
    return t
2×
265

266

267
def validate_document(t):
2×
268
    schema().assertValid(t)
2×
269

270

271
def request_vhost(request):
2×
272
    return request.headers.get('X-Forwarded-Host', request.headers.get('Host', request.base))
!
273

274

275
def request_scheme(request):
2×
276
    return request.headers.get('X-Forwarded-Proto', request.scheme)
!
277

278

279
def ensure_dir(fn):
2×
280
    d = os.path.dirname(fn)
2×
281
    if not os.path.exists(d):
2×
282
        os.makedirs(d)
!
283

284

285
def safe_write(fn, data, mkdirs=False):
2×
286
    """Safely write data to a file with name fn
287
    :param fn: a filename
288
    :param data: some string data to write
289
    :param mkdirs: create directories along the way (False by default)
290
    :return: True or False depending on the outcome of the write
291
    """
292
    tmpn = None
2×
293
    try:
2×
294
        fn = os.path.expanduser(fn)
2×
295
        dirname, basename = os.path.split(fn)
2×
296
        kwargs = dict(delete=False, prefix=".%s" % basename, dir=dirname)
2×
297
        if six.PY3:
2×
298
            kwargs['encoding'] = "utf-8"
2×
299
            mode = 'w+'
2×
300
        else:
301
            mode = 'w+b'
!
302

303
        if mkdirs:
2×
304
            ensure_dir(fn)
2×
305

306
        if isinstance(data, six.binary_type):
2×
307
            data = data.decode('utf-8')
2×
308

309
        with tempfile.NamedTemporaryFile(mode, **kwargs) as tmp:
2×
310
            if six.PY2:
2×
311
                data = data.encode('utf-8')
!
312

313
            log.debug("safe writing {} chrs into {}".format(len(data), fn))
2×
314
            tmp.write(data)
2×
315
            tmpn = tmp.name
2×
316
        if os.path.exists(tmpn) and os.stat(tmpn).st_size > 0:
2×
317
            os.rename(tmpn, fn)
2×
318
            # made these file readable by all
319
            os.chmod(fn, 0o644)
2×
320
            return True
2×
321
    except Exception as ex:
!
322
        log.debug(traceback.format_exc())
!
323
        log.error(ex)
!
324
    finally:
325
        if tmpn is not None and os.path.exists(tmpn):
2×
326
            try:
!
327
                os.unlink(tmpn)
!
328
            except Exception as ex:
!
329
                log.warn(ex)
!
330
    return False
!
331

332

333
def parse_date(s):
2×
334
    if s is None:
!
335
        return datetime.now()
!
336
    return datetime(*parsedate(s)[:6])
!
337

338

339
def root(t):
2×
340
    if hasattr(t, 'getroot') and hasattr(t.getroot, '__call__'):
2×
341
        return t.getroot()
2×
342
    else:
343
        return t
2×
344

345

346
def with_tree(elt, cb):
2×
347
    cb(elt)
2×
348
    if isinstance(elt.tag, six.string_types):
2×
349
        for child in list(elt):
2×
350
            with_tree(child, cb)
2×
351

352

353
def duration2timedelta(period):
2×
354
    regex = re.compile(
2×
355
        '(?P<sign>[-+]?)P(?:(?P<years>\d+)[Yy])?(?:(?P<months>\d+)[Mm])?(?:(?P<days>\d+)[Dd])?(?:T(?:(?P<hours>\d+)[Hh])?(?:(?P<minutes>\d+)[Mm])?(?:(?P<seconds>\d+)[Ss])?)?'
356
    )
357

358
    # Fetch the match groups with default value of 0 (not None)
359
    m = regex.match(period)
2×
360
    if not m:
2×
361
        return None
2×
362

363
    duration = m.groupdict(0)
2×
364

365
    # Create the timedelta object from extracted groups
366
    delta = timedelta(
2×
367
        days=int(duration['days']) + (int(duration['months']) * 30) + (int(duration['years']) * 365),
368
        hours=int(duration['hours']),
369
        minutes=int(duration['minutes']),
370
        seconds=int(duration['seconds']),
371
    )
372

373
    if duration['sign'] == "-":
2×
374
        delta *= -1
2×
375

376
    return delta
2×
377

378

379
def _lang(elt, default_lang):
2×
380
    return elt.get("{http://www.w3.org/XML/1998/namespace}lang", default_lang)
2×
381

382

383
def lang_dict(elts, getter=lambda e: e, default_lang=None):
2×
384
    if default_lang is None:
2×
385
        default_lang = config.langs[0]
2×
386

387
    r = dict()
2×
388
    for e in elts:
2×
389
        r[_lang(e, default_lang)] = getter(e)
2×
390
    return r
2×
391

392

393
def find_lang(elts, lang, default_lang):
2×
394
    return next((e for e in elts if _lang(e, default_lang) == lang), elts[0])
2×
395

396

397
def filter_lang(elts, langs=None):
2×
398
    if langs is None or type(langs) is not list:
2×
399
        langs = config.langs
2×
400

401
    # log.debug("langs: {}".format(langs))
402

403
    if elts is None:
2×
404
        return []
!
405

406
    elts = list(elts)
2×
407

408
    if len(elts) == 0:
2×
409
        return []
2×
410

411
    dflt = langs[0]
2×
412
    lst = [find_lang(elts, l, dflt) for l in langs]
2×
413
    if len(lst) > 0:
2×
414
        return lst
2×
415
    else:
416
        return elts
!
417

418

419
def xslt_transform(t, stylesheet, params=None):
2×
420
    if not params:
2×
421
        params = dict()
2×
422

423
    if not hasattr(thread_data, 'xslt'):
2×
424
        thread_data.xslt = dict()
2×
425

426
    transform = None
2×
427
    if stylesheet not in thread_data.xslt:
2×
428
        xsl = etree.fromstring(resource_string(stylesheet, "xslt"))
2×
429
        thread_data.xslt[stylesheet] = etree.XSLT(xsl)
2×
430
    transform = thread_data.xslt[stylesheet]
2×
431
    try:
2×
432
        return transform(t, **params)
2×
433
    except etree.XSLTApplyError as ex:
!
434
        for entry in transform.error_log:
!
435
            log.error('\tmessage from line %s, col %s: %s' % (entry.line, entry.column, entry.message))
!
436
            log.error('\tdomain: %s (%d)' % (entry.domain_name, entry.domain))
!
437
            log.error('\ttype: %s (%d)' % (entry.type_name, entry.type))
!
438
            log.error('\tlevel: %s (%d)' % (entry.level_name, entry.level))
!
439
            log.error('\tfilename: %s' % entry.filename)
!
440
        raise ex
!
441

442

443
def valid_until_ts(elt, default_ts):
2×
444
    ts = default_ts
!
445
    valid_until = elt.get("validUntil", None)
!
446
    if valid_until is not None:
!
447
        dt = iso8601.parse_date(valid_until)
!
448
        if dt is not None:
!
449
            ts = totimestamp(dt)
!
450

451
    cache_duration = elt.get("cacheDuration", None)
!
452
    if cache_duration is not None:
!
453
        dt = datetime.utcnow() + duration2timedelta(cache_duration)
!
454
        if dt is not None:
!
455
            ts = totimestamp(dt)
!
456

457
    return ts
!
458

459

460
def total_seconds(dt):
2×
461
    if hasattr(dt, "total_seconds"):
2×
462
        return dt.total_seconds()
2×
463
    else:
464
        return (dt.microseconds + (dt.seconds + dt.days * 24 * 3600) * 10 ** 6) / 10 ** 6
!
465

466

467
def etag(s):
2×
468
    return hex_digest(s, hn="sha256")
!
469

470

471
def hash_id(entity, hn='sha1', prefix=True):
2×
472
    entity_id = entity
2×
473
    if hasattr(entity, 'get'):
2×
474
        entity_id = entity.get('entityID')
2×
475

476
    hstr = hex_digest(entity_id, hn)
2×
477
    if prefix:
2×
478
        return "{%s}%s" % (hn, hstr)
2×
479
    else:
480
        return hstr
2×
481

482

483
def hex_digest(data, hn='sha1'):
2×
484
    if hn == 'null':
2×
485
        return data
2×
486

487
    if not hasattr(hashlib, hn):
2×
488
        raise ValueError("Unknown digest '%s'" % hn)
!
489

490
    if not isinstance(data, six.binary_type):
2×
491
        data = data.encode("utf-8")
2×
492

493
    m = getattr(hashlib, hn)()
2×
494
    m.update(data)
2×
495
    return m.hexdigest()
2×
496

497

498
def parse_xml(io, base_url=None):
2×
499
    huge_xml = config.huge_xml
2×
500
    return etree.parse(
2×
501
        io, base_url=base_url, parser=etree.XMLParser(resolve_entities=False, collect_ids=False, huge_tree=huge_xml)
502
    )
503

504

505
def has_tag(t, tag):
2×
506
    tags = t.iter(tag)
2×
507
    return next(tags, sentinel) is not sentinel
2×
508

509

510
def url2host(url):
2×
511
    (host, sep, port) = urlparse(url).netloc.partition(':')
2×
512
    return host
2×
513

514

515
def subdomains(domain):
2×
516
    dl = []
2×
517
    dsplit = domain.split('.')
2×
518
    if len(dsplit) < 3:
2×
519
        dl.append(domain)
2×
520
    else:
521
        for i in range(1, len(dsplit) - 1):
2×
522
            dl.append(".".join(dsplit[i:]))
2×
523

524
    return dl
2×
525

526

527
def ddist(a, b):
2×
528
    if len(a) > len(b):
!
529
        return ddist(b, a)
!
530

531
    a = a.split('.')
!
532
    b = b.split('.')
!
533

534
    d = [x[0] == x[1] for x in zip(a[::-1], b[::-1])]
!
535
    if False in d:
!
536
        return d.index(False)
!
537
    return len(a)
!
538

539

540
def avg_domain_distance(d1, d2):
2×
541
    dd = 0
!
542
    n = 0
!
543
    for a in d1.split(';'):
!
544
        for b in d2.split(';'):
!
545
            d = ddist(a, b)
!
546
            # log.debug("ddist %s %s -> %d" % (a, b, d))
547
            dd += d
!
548
            n += 1
!
549
    return int(dd / n)
!
550

551

552
def sync_nsmap(nsmap, elt):
2×
553
    fix = []
!
554
    for ns in elt.nsmap:
!
555
        if ns not in nsmap:
!
556
            nsmap[ns] = elt.nsmap[ns]
!
557
        elif nsmap[ns] != elt.nsmap[ns]:
!
558
            fix.append(ns)
!
559
        else:
560
            pass
!
561

562

563
def rreplace(s, old, new, occurrence):
2×
564
    li = s.rsplit(old, occurrence)
!
565
    return new.join(li)
!
566

567

568
def load_callable(name):
2×
569
    from importlib import import_module
2×
570

571
    p, m = name.rsplit(':', 1)
2×
572
    mod = import_module(p)
2×
573
    return getattr(mod, m)
2×
574

575

576
# semantics copied from https://github.com/lordal/md-summary/blob/master/md-summary
577
# many thanks to Anders Lordahl & Scotty Logan for the idea
578
def guess_entity_software(e):
2×
579
    for elt in chain(
!
580
        e.findall(".//{%s}SingleSignOnService" % NS['md']), e.findall(".//{%s}AssertionConsumerService" % NS['md'])
581
    ):
582
        location = elt.get('Location')
!
583
        if location:
!
584
            if (
!
585
                'Shibboleth.sso' in location
586
                or 'profile/SAML2/POST/SSO' in location
587
                or 'profile/SAML2/Redirect/SSO' in location
588
                or 'profile/Shibboleth/SSO' in location
589
            ):
590
                return 'Shibboleth'
!
591
            if location.endswith('saml2/idp/SSOService.php') or 'saml/sp/saml2-acs.php' in location:
!
592
                return 'SimpleSAMLphp'
!
593
            if location.endswith('user/authenticate'):
!
594
                return 'KalturaSSP'
!
595
            if location.endswith('adfs/ls') or location.endswith('adfs/ls/'):
!
596
                return 'ADFS'
!
597
            if '/oala/' in location or 'login.openathens.net' in location:
!
598
                return 'OpenAthens'
!
599
            if (
!
600
                '/idp/SSO.saml2' in location
601
                or '/sp/ACS.saml2' in location
602
                or 'sso.connect.pingidentity.com' in location
603
            ):
604
                return 'PingFederate'
!
605
            if 'idp/saml2/sso' in location:
!
606
                return 'Authentic2'
!
607
            if 'nidp/saml2/sso' in location:
!
608
                return 'Novell Access Manager'
!
609
            if 'affwebservices/public/saml2sso' in location:
!
610
                return 'CASiteMinder'
!
611
            if 'FIM/sps' in location:
!
612
                return 'IBMTivoliFIM'
!
613
            if (
!
614
                'sso/post' in location
615
                or 'sso/redirect' in location
616
                or 'saml2/sp/acs' in location
617
                or 'saml2/ls' in location
618
                or 'saml2/acs' in location
619
                or 'acs/redirect' in location
620
                or 'acs/post' in location
621
                or 'saml2/sp/ls/' in location
622
            ):
623
                return 'PySAML'
!
624
            if 'engine.surfconext.nl' in location:
!
625
                return 'SURFConext'
!
626
            if 'opensso' in location:
!
627
                return 'OpenSSO'
!
628
            if 'my.salesforce.com' in location:
!
629
                return 'Salesforce'
!
630

631
    entity_id = e.get('entityID')
!
632
    if '/shibboleth' in entity_id:
!
633
        return 'Shibboleth'
!
634
    if entity_id.endswith('/metadata.php'):
!
635
        return 'SimpleSAMLphp'
!
636
    if '/openathens' in entity_id:
!
637
        return 'OpenAthens'
!
638

639
    return 'other'
!
640

641

642
def is_text(x):
2×
643
    return isinstance(x, six.string_types) or isinstance(x, six.text_type)
2×
644

645

646
def chunks(l, n):
2×
647
    """Yield successive n-sized chunks from l."""
648
    for i in range(0, len(l), n):
!
649
        yield l[i : i + n]
!
650

651

652
class DirAdapter(BaseAdapter):
2×
653
    """
654
    An implementation of the requests Adapter interface that returns a the files in a directory. Used to simplify
655
    the code paths in pyFF and allows directories to be treated as yet another representation of a collection of metadata.
656
    """
657

658
    def send(self, request, **kwargs):
2×
659
        resp = Response()
2×
660
        (_, _, _dir) = request.url.partition('://')
2×
661
        if _dir is None or len(_dir) == 0:
2×
662
            raise ValueError("not a directory url: {}".format(request.url))
!
663
        resp.raw = six.BytesIO(six.b(_dir))
2×
664
        resp.status_code = 200
2×
665
        resp.reason = "OK"
2×
666
        resp.headers = {}
2×
667
        resp.url = request.url
2×
668

669
        return resp
2×
670

671
    def close(self):
2×
672
        pass
!
673

674

675
def url_get(url):
2×
676
    """
677
    Download an URL using a cache and return the response object
678
    :param url:
679
    :return:
680
    """
681

682
    s = None
2×
683
    if 'file://' in url:
2×
684
        s = requests.session()
2×
685
        s.mount('file://', FileAdapter())
2×
686
    elif 'dir://' in url:
2×
687
        s = requests.session()
2×
688
        s.mount('dir://', DirAdapter())
2×
689
    else:
690
        retry = Retry(total=3, backoff_factor=0.5)
2×
691
        adapter = HTTPAdapter(max_retries=retry)
2×
692
        s = CachedSession(
2×
693
            cache_name="pyff_cache",
694
            backend=config.request_cache_backend,
695
            expire_after=config.request_cache_time,
696
            old_data_on_error=True,
697
        )
698
        s.mount('http://', adapter)
2×
699
        s.mount('https://', adapter)
2×
700

701
    headers = {'User-Agent': "pyFF/{}".format(__version__), 'Accept': '*/*'}
2×
702
    _etag = None
2×
703
    if _etag is not None:
2×
704
        headers['If-None-Match'] = _etag
!
705
    try:
2×
706
        r = s.get(url, headers=headers, verify=False, timeout=config.request_timeout)
2×
707
    except IOError as ex:
2×
708
        s = requests.Session()
2×
709
        r = s.get(url, headers=headers, verify=False, timeout=config.request_timeout)
2×
710

711
    if six.PY2:
2×
712
        r.encoding = "utf-8"
!
713

714
    log.debug("url_get({}) returns {} chrs encoded as {}".format(url, len(r.content), r.encoding))
2×
715

716
    if config.request_override_encoding is not None:
2×
717
        r.encoding = config.request_override_encoding
2×
718

719
    return r
2×
720

721

722
def safe_b64e(data: Union[str, bytes]) -> str:
2×
723
    if not isinstance(data, bytes):
2×
UNCOV
724
        data = data.encode("utf-8")
!
725
    return base64.b64encode(data).decode('ascii')
2×
726

727

728
def safe_b64d(s: str) -> bytes:
2×
729
    return base64.b64decode(s)
!
730

731

732
# data:&lt;class 'type'&gt;;base64,
733
# data:<class 'type'>;base64,
734

735

736
def img_to_data(data: bytes, content_type: str) -> Optional[str]:
2×
737
    """Convert a file (specified by a path) into a data URI."""
738
    mime_type, options = cgi.parse_header(content_type)
2×
739
    data64 = None
2×
740
    if len(data) > config.icon_maxsize:
2×
741
        return None
!
742

743
    try:
2×
744
        from PIL import Image
2×
745
    except ImportError:
2×
746
        Image = None
2×
747

748
    if Image is not None:
2×
749
        try:
!
750
            im = Image.open(io.BytesIO(data))
!
751
            if im.format not in ('PNG', 'SVG'):
!
752
                out = io.BytesIO()
!
753
                im.save(out, format="PNG")
!
754
                data64 = safe_b64e(out.getvalue())
!
755
                assert data64
!
756
                mime_type = "image/png"
!
757
        except BaseException as ex:
!
758
            log.warn(ex)
!
759
            log.debug(traceback.format_exc())
!
760

761
    if data64 is None or len(data64) == 0:
2×
762
        data64 = safe_b64e(data)
2×
763
    return 'data:{};base64,{}'.format(mime_type, data64)
2×
764

765

766
def short_id(data):
2×
767
    hasher = hashlib.sha1(data)
!
768
    return base64.urlsafe_b64encode(hasher.digest()[0:10]).rstrip('=')
!
769

770

771
def unicode_stream(data):
2×
772
    return six.BytesIO(data.encode('UTF-8'))
2×
773

774

775
def b2u(data):
2×
776
    if is_text(data):
2×
777
        return data
2×
778
    elif isinstance(data, six.binary_type):
2×
779
        return data.decode("utf-8")
2×
780
    elif isinstance(data, tuple) or isinstance(data, list):
2×
781
        return [b2u(item) for item in data]
2×
782
    elif isinstance(data, set):
2×
783
        return set([b2u(item) for item in data])
!
784
    return data
2×
785

786

787
def json_serializer(o):
2×
788
    if isinstance(o, datetime):
2×
789
        return o.__str__()
2×
790
    if isinstance(o, CaseInsensitiveDict):
!
791
        return dict(o.items())
!
792
    if isinstance(o, BaseException):
!
793
        return str(o)
!
794
    if hasattr(o, 'to_json') and hasattr(o.to_json, '__call__'):
!
795
        return o.to_json()
!
796
    if isinstance(o, threading.Thread):
!
797
        return o.name
!
798

799
    raise ValueError("Object {} of type {} is not JSON-serializable via this function".format(repr(o), type(o)))
!
800

801

802
class Lambda(object):
2×
803
    def __init__(self, cb, *args, **kwargs):
2×
804
        self._cb = cb
2×
805
        self._args = [a for a in args]
2×
806
        self._kwargs = kwargs or {}
2×
807

808
    def __call__(self, *args, **kwargs):
2×
809
        args = [a for a in args]
2×
810
        args.extend(self._args)
2×
811
        kwargs.update(self._kwargs)
2×
812
        return self._cb(*args, **kwargs)
2×
813

814

815
@contextlib.contextmanager
2×
816
def non_blocking_lock(lock=threading.Lock(), exception_class=ResourceException, args=("Resource is busy",)):
2×
817
    if not lock.acquire(blocking=False):
2×
818
        raise exception_class(*args)
!
819
    try:
2×
820
        yield lock
2×
821
    finally:
822
        lock.release()
2×
823

824

825
def make_default_scheduler():
2×
826
    if config.scheduler_job_store == 'redis':
2×
827
        jobstore = RedisJobStore(host=config.redis_host, port=config.redis_port)
!
828
    elif config.scheduler_job_store == 'memory':
2×
829
        jobstore = MemoryJobStore()
2×
830
    else:
831
        raise ValueError("unknown or unsupported job store type '{}'".format(config.scheduler_job_store))
!
832
    return BackgroundScheduler(
2×
833
        executors={'default': ThreadPoolExecutor(config.worker_pool_size)},
834
        jobstores={'default': jobstore},
835
        job_defaults={'misfire_grace_time': config.update_frequency},
836
    )
837

838

839
class MappingStack(Mapping):
2×
840
    def __init__(self, *args):
2×
841
        self._m = list(args)
!
842

843
    def __contains__(self, item):
2×
844
        return any([item in d for d in self._m])
!
845

846
    def __getitem__(self, item):
2×
847
        for d in self._m:
!
848
            log.debug("----")
!
849
            log.debug(repr(d))
!
850
            log.debug(repr(item))
!
851
            log.debug("++++")
!
852
            if item in d:
!
853
                return d[item]
!
854
        return None
!
855

856
    def __iter__(self):
2×
857
        for d in self._m:
!
858
            for item in d:
!
859
                yield item
!
860

861
    def __len__(self) -> int:
2×
862
        return sum([len(d) for d in self._m])
!
863

864

865
class LRUProxyDict(MutableMapping):
2×
866
    def __init__(self, proxy, *args, **kwargs):
2×
867
        self._proxy = proxy
2×
868
        self._cache = LRUCache(**kwargs)
2×
869

870
    def __contains__(self, item):
2×
871
        return item in self._cache or item in self._proxy
2×
872

873
    def __getitem__(self, item):
2×
874
        if item is None:
2×
875
            raise ValueError("None key")
!
876
        v = self._cache.get(item, None)
2×
877
        if v is not None:
2×
878
            return v
2×
879
        v = self._proxy.get(item, None)
2×
880
        if v is not None:
2×
881
            self._cache[item] = v
2×
882
        return v
2×
883

884
    def __setitem__(self, key, value):
2×
885
        self._proxy[key] = value
2×
886
        self._cache[key] = value
2×
887

888
    def __delitem__(self, key):
2×
889
        self._proxy.pop(key, None)
!
890
        self._cache.pop(key, None)
!
891

892
    def __iter__(self):
2×
893
        return self._proxy.__iter__()
2×
894

895
    def __len__(self):
2×
896
        return len(self._proxy)
2×
897

898

899
def find_matching_files(d, extensions):
2×
900
    for top, dirs, files in os.walk(d):
2×
901
        for dn in dirs:
2×
902
            if dn.startswith("."):
2×
903
                dirs.remove(dn)
!
904

905
        for nm in files:
2×
906
            (_, _, ext) = nm.rpartition('.')
2×
907
            if ext in extensions:
2×
908
                fn = os.path.join(top, nm)
2×
909
                yield fn
2×
910

911

912
def is_past_ttl(last_seen, ttl=config.cache_ttl):
2×
913
    fuzz = ttl
2×
914
    now = int(time.time())
2×
915
    if config.randomize_cache_ttl:
2×
916
        fuzz = random.randrange(1, ttl)
2×
917
    return now > int(last_seen) + fuzz
2×
918

919

920
class Watchable(object):
2×
921
    class Watcher(object):
2×
922
        def __init__(self, cb, args, kwargs):
2×
923
            self.cb = cb
2×
924
            self.args = args
2×
925
            self.kwargs = kwargs
2×
926

927
        def __call__(self, *args, **kwargs):
2×
928
            kwargs_copy = copy(kwargs)
2×
929
            args_copy = copy(list(args))
2×
930
            kwargs_copy.update(self.kwargs)
2×
931
            args_copy.extend(self.args)
2×
932
            return self.cb(*args_copy, **kwargs_copy)
2×
933

934
        def __cmp__(self, other):
2×
935
            return other.cb == self.cb
!
936

937
    def __init__(self):
2×
938
        self.watchers = []
2×
939

940
    def add_watcher(self, cb, *args, **kwargs):
2×
941
        self.watchers.append(Watchable.Watcher(cb, args, kwargs))
2×
942

943
    def remove_watcher(self, cb, *args, **kwargs):
2×
944
        self.watchers.remove(Watchable.Watcher(cb))
!
945

946
    def notify(self, *args, **kwargs):
2×
947
        kwargs['watched'] = self
2×
948
        for cb in self.watchers:
2×
949
            try:
2×
950
                cb(*args, **kwargs)
2×
951
            except BaseException as ex:
!
952
                log.debug(traceback.format_exc())
!
953
                log.warn(ex)
!
Troubleshooting · Open an Issue · Sales · Support · ENTERPRISE · CAREERS · STATUS
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2023 Coveralls, Inc