typeworld / typeworld
1
# -*- coding: utf-8 -*-
2

3 3
import json
4 3
import copy
5 3
import types
6 3
import inspect
7 3
import re
8 3
import traceback
9 3
import datetime
10 3
import markdown2
11 3
import semver
12 3
import functools
13 3
import platform
14

15

16
###############################################################################
17
###############################################################################
18
###############################################################################
19
###############################################################################
20
###############################################################################
21
###############################################################################
22

23

24
#  Constants
25

26 3
VERSION = "0.2.9-beta"
27 3
BREAKINGVERSIONS = ["0.2.9-beta"]
28

29 3
WIN = platform.system() == "Windows"
30 3
MAC = platform.system() == "Darwin"
31 3
LINUX = platform.system() == "Linux"
32

33
# Response types (success, error, ...)
34 3
SUCCESS = "success"
35 3
ERROR = "error"
36

37 3
UNKNOWNFONT = "unknownFont"
38 3
INSUFFICIENTPERMISSION = "insufficientPermission"
39 3
SEATALLOWANCEREACHED = "seatAllowanceReached"
40 3
UNKNOWNINSTALLATION = "unknownInstallation"
41 3
NOFONTSAVAILABLE = "noFontsAvailable"
42 3
TEMPORARILYUNAVAILABLE = "temporarilyUnavailable"
43 3
VALIDTYPEWORLDUSERACCOUNTREQUIRED = "validTypeWorldUserAccountRequired"
44 3
REVEALEDUSERIDENTITYREQUIRED = "revealedUserIdentityRequired"
45 3
LOGINREQUIRED = "loginRequired"
46

47 3
PROTOCOLS = ["typeworld"]
48

49

50 3
RESPONSES = {
51
    SUCCESS: "The request has been processed successfully.",
52
    ERROR: (
53
        "There request produced an error. You may add a custom error "
54
        "message in the `errorMessage` field."
55
    ),
56
    UNKNOWNFONT: "No font could be identified for the given `fontID`.",
57
    INSUFFICIENTPERMISSION: (
58
        "The Type.World user account credentials "
59
        "couldn’t be confirmed by the publisher (which are checked with the "
60
        "central server) and therefore access to the subscription is denied."
61
    ),
62
    SEATALLOWANCEREACHED: (
63
        "The user has exhausted their seat allowances for "
64
        "this font. The app may take them to the publisher’s website as "
65
        "defined in ::LicenseUsage.upgradeURL:: to upgrade their font license."
66
    ),
67
    UNKNOWNINSTALLATION: (
68
        "This font installation (combination of app instance and user "
69
        "credentials) is unknown. The response with this error message is "
70
        "crucial to remote de-authorization of app instances. When a user "
71
        "de-authorizes an entire app instance’s worth of font installations, "
72
        "such as when a computer got bricked and re-installed or is lost, the "
73
        "success of the remote de-authorization process is judged by either "
74
        "`success` responses (app actually had this font installed and its "
75
        "deletion has been recorded) or `unknownInstallation` responses "
76
        "(app didn’t have this font installed). All other reponses count as "
77
        "errors in the remote de-authorization process."
78
    ),
79
    NOFONTSAVAILABLE: ("This subscription exists but carries no fonts at the moment."),
80
    TEMPORARILYUNAVAILABLE: (
81
        "The service is temporarily unavailable but should work again later on."
82
    ),
83
    VALIDTYPEWORLDUSERACCOUNTREQUIRED: (
84
        "The access to this subscription requires a valid Type.World user "
85
        "account connected to an app."
86
    ),
87
    REVEALEDUSERIDENTITYREQUIRED: (
88
        "The access to this subscription requires a valid Type.World user "
89
        "account and that the user agrees to having their identity "
90
        "(name and email address) submitted to the publisher upon font "
91
        "installation (closed workgroups only)."
92
    ),
93
    LOGINREQUIRED: (
94
        "The access to this subscription requires that the user logs into "
95
        "the publisher’s website again to authenticate themselves. "
96
        "Normally, this happens after a subscription’s secret key has been "
97
        "invalidated. The user will be taken to the publisher’s website "
98
        "defined at ::EndpointResponse.loginURL::. After successful login, "
99
        "a button should be presented to the user to reconnect to the same "
100
        "subscription that they are trying to access. To identify the "
101
        "subscription, the link that the user will be taken to will carry a "
102
        "`subscriptionID` parameter with the subscriptionID as defined in "
103
        "the subscription’s URL."
104
    ),
105
}
106

107
# Commands
108 3
ENDPOINTCOMMAND = {
109
    "keyword": "endpoint",
110
    "currentVersion": VERSION,
111
    "responseTypes": [SUCCESS, ERROR],
112
    "acceptableMimeTypes": ["application/json"],
113
}
114 3
INSTALLABLEFONTSCOMMAND = {
115
    "keyword": "installableFonts",
116
    "currentVersion": VERSION,
117
    "responseTypes": [
118
        SUCCESS,
119
        ERROR,
120
        NOFONTSAVAILABLE,
121
        INSUFFICIENTPERMISSION,
122
        TEMPORARILYUNAVAILABLE,
123
        VALIDTYPEWORLDUSERACCOUNTREQUIRED,
124
    ],
125
    "acceptableMimeTypes": ["application/json"],
126
}
127 3
INSTALLFONTSCOMMAND = {
128
    "keyword": "installFonts",
129
    "currentVersion": VERSION,
130
    "responseTypes": [
131
        SUCCESS,
132
        ERROR,
133
        INSUFFICIENTPERMISSION,
134
        TEMPORARILYUNAVAILABLE,
135
        VALIDTYPEWORLDUSERACCOUNTREQUIRED,
136
        LOGINREQUIRED,
137
        REVEALEDUSERIDENTITYREQUIRED,
138
    ],
139
    "acceptableMimeTypes": ["application/json"],
140
}
141 3
UNINSTALLFONTSCOMMAND = {
142
    "keyword": "uninstallFonts",
143
    "currentVersion": VERSION,
144
    "responseTypes": [
145
        SUCCESS,
146
        ERROR,
147
        INSUFFICIENTPERMISSION,
148
        TEMPORARILYUNAVAILABLE,
149
        VALIDTYPEWORLDUSERACCOUNTREQUIRED,
150
        LOGINREQUIRED,
151
    ],
152
    "acceptableMimeTypes": ["application/json"],
153
}
154

155 3
INSTALLFONTASSETCOMMAND = {
156
    "responseTypes": [
157
        SUCCESS,
158
        ERROR,
159
        UNKNOWNFONT,
160
        INSUFFICIENTPERMISSION,
161
        TEMPORARILYUNAVAILABLE,
162
        VALIDTYPEWORLDUSERACCOUNTREQUIRED,
163
        LOGINREQUIRED,
164
        REVEALEDUSERIDENTITYREQUIRED,
165
        SEATALLOWANCEREACHED,
166
    ],
167
}
168 3
UNINSTALLFONTASSETCOMMAND = {
169
    "responseTypes": [
170
        SUCCESS,
171
        ERROR,
172
        UNKNOWNFONT,
173
        INSUFFICIENTPERMISSION,
174
        TEMPORARILYUNAVAILABLE,
175
        VALIDTYPEWORLDUSERACCOUNTREQUIRED,
176
        LOGINREQUIRED,
177
        UNKNOWNINSTALLATION,
178
    ],
179
}
180

181 3
COMMANDS = [
182
    ENDPOINTCOMMAND,
183
    INSTALLABLEFONTSCOMMAND,
184
    INSTALLFONTSCOMMAND,
185
    UNINSTALLFONTSCOMMAND,
186
]
187

188

189 3
FONTPURPOSES = {
190
    "desktop": {
191
        "acceptableMimeTypes": [
192
            "font/collection",
193
            "font/otf",
194
            "font/sfnt",
195
            "font/ttf",
196
        ],
197
    },
198
    "web": {"acceptableMimeTypes": ["application/zip"]},
199
    "app": {"acceptableMimeTypes": ["application/zip"]},
200
}
201

202
# https://tools.ietf.org/html/rfc8081
203

204 3
MIMETYPES = {
205
    "font/sfnt": {"fileExtensions": ["otf", "ttf"]},
206
    "font/ttf": {"fileExtensions": ["ttf"]},
207
    "font/otf": {"fileExtensions": ["otf"]},
208
    "font/collection": {"fileExtensions": ["ttc"]},
209
    "font/woff": {"fileExtensions": ["woff"]},
210
    "font/woff2": {"fileExtensions": ["woff2"]},
211
}
212

213
# Compile list of file extensions
214 3
FILEEXTENSIONS = []
215 3
for mimeType in list(MIMETYPES.keys()):
216 3
    FILEEXTENSIONS = list(
217
        set(FILEEXTENSIONS) | set(MIMETYPES[mimeType]["fileExtensions"])
218
    )
219

220 3
FILEEXTENSIONNAMES = {
221
    "otf": "OpenType",
222
    "ttf": "TrueType",
223
    "ttc": "TrueType collection",
224
    "woff": "WOFF",
225
    "woff2": "WOFF2",
226
}
227

228 3
MIMETYPEFORFONTTYPE = {
229
    "otf": "font/otf",
230
    "ttf": "font/ttf",
231
    "ttc": "font/collection",
232
    "woff": "font/woff",
233
    "woff2": "font/woff2",
234
}
235

236 3
FONTENCODINGS = ["base64"]
237

238 3
OPENSOURCELICENSES = [
239
    "0BSD",
240
    "AAL",
241
    "Abstyles",
242
    "Adobe-2006",
243
    "Adobe-Glyph",
244
    "ADSL",
245
    "AFL-1.1",
246
    "AFL-1.2",
247
    "AFL-2.0",
248
    "AFL-2.1",
249
    "AFL-3.0",
250
    "Afmparse",
251
    "AGPL-1.0",
252
    "AGPL-3.0-only",
253
    "AGPL-3.0-or-later",
254
    "Aladdin",
255
    "AMDPLPA",
256
    "AML",
257
    "AMPAS",
258
    "ANTLR-PD",
259
    "Apache-1.0",
260
    "Apache-1.1",
261
    "Apache-2.0",
262
    "APAFML",
263
    "APL-1.0",
264
    "APSL-1.0",
265
    "APSL-1.1",
266
    "APSL-1.2",
267
    "APSL-2.0",
268
    "Artistic-1.0-cl8",
269
    "Artistic-1.0-Perl",
270
    "Artistic-1.0",
271
    "Artistic-2.0",
272
    "Bahyph",
273
    "Barr",
274
    "Beerware",
275
    "BitTorrent-1.0",
276
    "BitTorrent-1.1",
277
    "Borceux",
278
    "BSD-1-Clause",
279
    "BSD-2-Clause-FreeBSD",
280
    "BSD-2-Clause-NetBSD",
281
    "BSD-2-Clause-Patent",
282
    "BSD-2-Clause",
283
    "BSD-3-Clause-Attribution",
284
    "BSD-3-Clause-Clear",
285
    "BSD-3-Clause-LBNL",
286
    "BSD-3-Clause-No-Nuclear-License-2014",
287
    "BSD-3-Clause-No-Nuclear-License",
288
    "BSD-3-Clause-No-Nuclear-Warranty",
289
    "BSD-3-Clause",
290
    "BSD-4-Clause-UC",
291
    "BSD-4-Clause",
292
    "BSD-Protection",
293
    "BSD-Source-Code",
294
    "BSL-1.0",
295
    "bzip2-1.0.5",
296
    "bzip2-1.0.6",
297
    "Caldera",
298
    "CATOSL-1.1",
299
    "CC-BY-1.0",
300
    "CC-BY-2.0",
301
    "CC-BY-2.5",
302
    "CC-BY-3.0",
303
    "CC-BY-4.0",
304
    "CC-BY-NC-1.0",
305
    "CC-BY-NC-2.0",
306
    "CC-BY-NC-2.5",
307
    "CC-BY-NC-3.0",
308
    "CC-BY-NC-4.0",
309
    "CC-BY-NC-ND-1.0",
310
    "CC-BY-NC-ND-2.0",
311
    "CC-BY-NC-ND-2.5",
312
    "CC-BY-NC-ND-3.0",
313
    "CC-BY-NC-ND-4.0",
314
    "CC-BY-NC-SA-1.0",
315
    "CC-BY-NC-SA-2.0",
316
    "CC-BY-NC-SA-2.5",
317
    "CC-BY-NC-SA-3.0",
318
    "CC-BY-NC-SA-4.0",
319
    "CC-BY-ND-1.0",
320
    "CC-BY-ND-2.0",
321
    "CC-BY-ND-2.5",
322
    "CC-BY-ND-3.0",
323
    "CC-BY-ND-4.0",
324
    "CC-BY-SA-1.0",
325
    "CC-BY-SA-2.0",
326
    "CC-BY-SA-2.5",
327
    "CC-BY-SA-3.0",
328
    "CC-BY-SA-4.0",
329
    "CC0-1.0",
330
    "CDDL-1.0",
331
    "CDDL-1.1",
332
    "CDLA-Permissive-1.0",
333
    "CDLA-Sharing-1.0",
334
    "CECILL-1.0",
335
    "CECILL-1.1",
336
    "CECILL-2.0",
337
    "CECILL-2.1",
338
    "CECILL-B",
339
    "CECILL-C",
340
    "ClArtistic",
341
    "CNRI-Jython",
342
    "CNRI-Python-GPL-Compatible",
343
    "CNRI-Python",
344
    "Condor-1.1",
345
    "CPAL-1.0",
346
    "CPL-1.0",
347
    "CPOL-1.02",
348
    "Crossword",
349
    "CrystalStacker",
350
    "CUA-OPL-1.0",
351
    "Cube",
352
    "curl",
353
    "D-FSL-1.0",
354
    "diffmark",
355
    "DOC",
356
    "Dotseqn",
357
    "DSDP",
358
    "dvipdfm",
359
    "ECL-1.0",
360
    "ECL-2.0",
361
    "EFL-1.0",
362
    "EFL-2.0",
363
    "eGenix",
364
    "Entessa",
365
    "EPL-1.0",
366
    "EPL-2.0",
367
    "ErlPL-1.1",
368
    "EUDatagrid",
369
    "EUPL-1.0",
370
    "EUPL-1.1",
371
    "EUPL-1.2",
372
    "Eurosym",
373
    "Fair",
374
    "Frameworx-1.0",
375
    "FreeImage",
376
    "FSFAP",
377
    "FSFUL",
378
    "FSFULLR",
379
    "FTL",
380
    "GFDL-1.1-only",
381
    "GFDL-1.1-or-later",
382
    "GFDL-1.2-only",
383
    "GFDL-1.2-or-later",
384
    "GFDL-1.3-only",
385
    "GFDL-1.3-or-later",
386
    "Giftware",
387
    "GL2PS",
388
    "Glide",
389
    "Glulxe",
390
    "gnuplot",
391
    "GPL-1.0-only",
392
    "GPL-1.0-or-later",
393
    "GPL-2.0-only",
394
    "GPL-2.0-or-later",
395
    "GPL-3.0-only",
396
    "GPL-3.0-or-later",
397
    "gSOAP-1.3b",
398
    "HaskellReport",
399
    "HPND",
400
    "IBM-pibs",
401
    "ICU",
402
    "IJG",
403
    "ImageMagick",
404
    "iMatix",
405
    "Imlib2",
406
    "Info-ZIP",
407
    "Intel-ACPI",
408
    "Intel",
409
    "Interbase-1.0",
410
    "IPA",
411
    "IPL-1.0",
412
    "ISC",
413
    "JasPer-2.0",
414
    "JSON",
415
    "LAL-1.2",
416
    "LAL-1.3",
417
    "Latex2e",
418
    "Leptonica",
419
    "LGPL-2.0-only",
420
    "LGPL-2.0-or-later",
421
    "LGPL-2.1-only",
422
    "LGPL-2.1-or-later",
423
    "LGPL-3.0-only",
424
    "LGPL-3.0-or-later",
425
    "LGPLLR",
426
    "Libpng",
427
    "libtiff",
428
    "LiLiQ-P-1.1",
429
    "LiLiQ-R-1.1",
430
    "LiLiQ-Rplus-1.1",
431
    "LPL-1.0",
432
    "LPL-1.02",
433
    "LPPL-1.0",
434
    "LPPL-1.1",
435
    "LPPL-1.2",
436
    "LPPL-1.3a",
437
    "LPPL-1.3c",
438
    "MakeIndex",
439
    "MirOS",
440
    "MIT-advertising",
441
    "MIT-CMU",
442
    "MIT-enna",
443
    "MIT-feh",
444
    "MIT",
445
    "MITNFA",
446
    "Motosoto",
447
    "mpich2",
448
    "MPL-1.0",
449
    "MPL-1.1",
450
    "MPL-2.0-no-copyleft-exception",
451
    "MPL-2.0",
452
    "MS-PL",
453
    "MS-RL",
454
    "MTLL",
455
    "Multics",
456
    "Mup",
457
    "NASA-1.3",
458
    "Naumen",
459
    "NBPL-1.0",
460
    "NCSA",
461
    "Net-SNMP",
462
    "NetCDF",
463
    "Newsletr",
464
    "NGPL",
465
    "NLOD-1.0",
466
    "NLPL",
467
    "Nokia",
468
    "NOSL",
469
    "Noweb",
470
    "NPL-1.0",
471
    "NPL-1.1",
472
    "NPOSL-3.0",
473
    "NRL",
474
    "NTP",
475
    "OCCT-PL",
476
    "OCLC-2.0",
477
    "ODbL-1.0",
478
    "OFL-1.0",
479
    "OFL-1.1",
480
    "OGTSL",
481
    "OLDAP-1.1",
482
    "OLDAP-1.2",
483
    "OLDAP-1.3",
484
    "OLDAP-1.4",
485
    "OLDAP-2.0.1",
486
    "OLDAP-2.0",
487
    "OLDAP-2.1",
488
    "OLDAP-2.2.1",
489
    "OLDAP-2.2.2",
490
    "OLDAP-2.2",
491
    "OLDAP-2.3",
492
    "OLDAP-2.4",
493
    "OLDAP-2.5",
494
    "OLDAP-2.6",
495
    "OLDAP-2.7",
496
    "OLDAP-2.8",
497
    "OML",
498
    "OpenSSL",
499
    "OPL-1.0",
500
    "OSET-PL-2.1",
501
    "OSL-1.0",
502
    "OSL-1.1",
503
    "OSL-2.0",
504
    "OSL-2.1",
505
    "OSL-3.0",
506
    "PDDL-1.0",
507
    "PHP-3.0",
508
    "PHP-3.01",
509
    "Plexus",
510
    "PostgreSQL",
511
    "psfrag",
512
    "psutils",
513
    "Python-2.0",
514
    "Qhull",
515
    "QPL-1.0",
516
    "Rdisc",
517
    "RHeCos-1.1",
518
    "RPL-1.1",
519
    "RPL-1.5",
520
    "RPSL-1.0",
521
    "RSA-MD",
522
    "RSCPL",
523
    "Ruby",
524
    "SAX-PD",
525
    "Saxpath",
526
    "SCEA",
527
    "Sendmail",
528
    "SGI-B-1.0",
529
    "SGI-B-1.1",
530
    "SGI-B-2.0",
531
    "SimPL-2.0",
532
    "SISSL-1.2",
533
    "SISSL",
534
    "Sleepycat",
535
    "SMLNJ",
536
    "SMPPL",
537
    "SNIA",
538
    "Spencer-86",
539
    "Spencer-94",
540
    "Spencer-99",
541
    "SPL-1.0",
542
    "SugarCRM-1.1.3",
543
    "SWL",
544
    "TCL",
545
    "TCP-wrappers",
546
    "TMate",
547
    "TORQUE-1.1",
548
    "TOSL",
549
    "Unicode-DFS-2015",
550
    "Unicode-DFS-2016",
551
    "Unicode-TOU",
552
    "Unlicense",
553
    "UPL-1.0",
554
    "Vim",
555
    "VOSTROM",
556
    "VSL-1.0",
557
    "W3C-19980720",
558
    "W3C-20150513",
559
    "W3C",
560
    "Watcom-1.0",
561
    "Wsuipa",
562
    "WTFPL",
563
    "X11",
564
    "Xerox",
565
    "XFree86-1.1",
566
    "xinetd",
567
    "Xnet",
568
    "xpp",
569
    "XSkat",
570
    "YPL-1.0",
571
    "YPL-1.1",
572
    "Zed",
573
    "Zend-2.0",
574
    "Zimbra-1.3",
575
    "Zimbra-1.4",
576
    "zlib-acknowledgement",
577
    "Zlib",
578
    "ZPL-1.1",
579
    "ZPL-2.0",
580
    "ZPL-2.1",
581
]
582

583 3
FONTSTATUSES = ["prerelease", "trial", "stable"]
584

585 3
PUBLISHERTYPES = ["free", "retail", "custom", "undefined"]
586 3
PUBLICPUBLISHERTYPES = ["free", "retail", "custom"]
587

588 3
PUBLISHERSIDEAPPANDUSERCREDENTIALSTATUSES = ["active", "deleted", "revoked"]
589

590 3
DEFAULT = "__default__"
591

592

593
###############################################################################
594
###############################################################################
595
###############################################################################
596
###############################################################################
597
###############################################################################
598
###############################################################################
599

600
#  Helper methods
601

602

603 3
def makeSemVer(version):
604
    """Turn simple float number (0.1) into semver-compatible number
605
    for comparison by adding .0(s): (0.1.0)"""
606

607
    # Make string
608 3
    version = str(version)
609

610 3
    if version.count(".") < 2:
611

612
        # Strip leading zeros
613 3
        version = ".".join(map(str, list(map(int, version.split(".")))))
614

615
        # Add .0(s)
616 3
        version = version + (2 - version.count(".")) * ".0"
617

618 3
    return version
619

620

621 3
def ResponsesDocu(responses):
622

623 3
    text = "\n\n"
624

625 3
    for response in responses:
626

627 3
        text += "`%s`: %s\n\n" % (response, RESPONSES[response])
628

629 3
    return text
630

631

632
###############################################################################
633
###############################################################################
634
###############################################################################
635
###############################################################################
636
###############################################################################
637
###############################################################################
638

639
#  Basic Data Types
640

641

642 3
class DataType(object):
643 3
    initialData = None
644 3
    dataType = None
645

646 3
    def __init__(self):
647 3
        self.value = copy.copy(self.initialData)
648

649 3
        if issubclass(self.__class__, (MultiLanguageText, MultiLanguageTextProxy)):
650 3
            self.value = self.dataType()
651

652 3
    def __repr__(self):
653 3
        if issubclass(self.__class__, Proxy):
654 3
            return "<%s>" % (self.dataType.__name__)
655
        else:
656 3
            return "<%s '%s'>" % (self.__class__.__name__, self.get())
657

658 3
    def valid(self):
659 3
        if not self.value:
660 3
            return True
661

662 3
        if type(self.value) == self.dataType:
663 3
            return True
664
        else:
665 3
            return "Wrong data type. Is %s, should be: %s." % (
666
                type(self.value),
667
                self.dataType,
668
            )
669

670 3
    def get(self):
671 3
        return self.value
672

673 3
    def put(self, value):
674

675 3
        self.value = self.shapeValue(value)
676

677 3
        if issubclass(
678
            self.value.__class__, (DictBasedObject, ListProxy, Proxy, DataType)
679
        ):
680 3
            object.__setattr__(self.value, "_parent", self)
681

682 3
        valid = self.valid()
683 3
        if valid is not True and valid is not None:
684 3
            raise ValueError(valid)
685

686 3
    def shapeValue(self, value):
687 3
        return value
688

689 3
    def isEmpty(self):
690 3
        return self.value is None or self.value == [] or self.value == ""
691

692 3
    def isSet(self):
693 3
        return not self.isEmpty()
694

695 3
    def formatHint(self):
696 3
        return None
697

698 3
    def exampleData(self):
699 3
        return None
700

701

702 3
class BooleanDataType(DataType):
703 3
    dataType = bool
704

705

706 3
class IntegerDataType(DataType):
707 3
    dataType = int
708

709 3
    def shapeValue(self, value):
710 3
        return int(value)
711

712

713 3
class FloatDataType(DataType):
714 3
    dataType = float
715

716 3
    def shapeValue(self, value):
717 3
        return float(value)
718

719

720 3
class StringDataType(DataType):
721 3
    dataType = str
722

723 3
    def shapeValue(self, value):
724 3
        return str(value)
725

726

727 3
class DictionaryDataType(DataType):
728 3
    dataType = dict
729

730 3
    def shapeValue(self, value):
731 3
        return dict(value)
732

733

734 3
class FontDataType(StringDataType):
735 3
    pass
736

737

738 3
class FontEncodingDataType(StringDataType):
739 3
    def valid(self):
740 3
        if not self.value:
741 3
            return True
742

743 3
        if self.value not in FONTENCODINGS:
744 3
            return "Encoding '%s' is unknown. Known are: %s" % (
745
                self.value,
746
                FONTENCODINGS,
747
            )
748

749 3
        return True
750

751

752 3
class VersionDataType(StringDataType):
753 3
    dataType = str
754

755 3
    def valid(self):
756 3
        if not self.value:
757 3
            return True
758

759
        # Append .0 for semver comparison
760 3
        try:
761 3
            value = makeSemVer(self.value)
762 3
        except ValueError:
763 3
            return False
764

765 3
        try:
766 3
            semver.VersionInfo.parse(value)
767 3
        except ValueError as e:
768 3
            return str(e)
769 3
        return True
770

771 3
    def formatHint(self):
772 3
        return (
773
            "Simple float number (1 or 1.01) or semantic versioning "
774
            "(2.0.0-rc.1) as per [semver.org](https://semver.org)"
775
        )
776

777

778 3
class TimestampDataType(IntegerDataType):
779 3
    pass
780

781

782 3
class DateDataType(StringDataType):
783 3
    def valid(self):
784 3
        if not self.value:
785 3
            return True
786

787 3
        try:
788 3
            datetime.datetime.strptime(self.value, "%Y-%m-%d")
789 3
            return True
790

791 3
        except ValueError:
792 3
            return traceback.format_exc().splitlines()[-1]
793

794 3
    def formatHint(self):
795 3
        return "YYYY-MM-DD"
796

797

798 3
class WebURLDataType(StringDataType):
799 3
    def valid(self):
800 3
        if not self.value:
801 3
            return True
802

803 3
        if not self.value.startswith("http://") and not self.value.startswith(
804
            "https://"
805
        ):
806 3
            return "Needs to start with http:// or https://"
807
        else:
808 3
            return True
809

810

811
# # TODO: This is a stump. Expand.
812
# class TypeWorldURLDataType(StringDataType):
813
#     def valid(self):
814
#         if not self.value:
815
#             return True
816

817
#         if not self.value.startswith("http://") and not self.value.startswith(
818
#             "https://"
819
#         ):
820
#             return "Needs to start with http:// or https://"
821
#         else:
822
#             return True
823

824
#     def formatHint(self):
825
#         return (
826
#             "Type.World Subscription URL as per "
827
#             "[Developer Docs](https://type.world/developer#the-subscription-url)"
828
#         )
829

830

831 3
class TelephoneDataType(StringDataType):
832 3
    def valid(self):
833 3
        if not self.value:
834 0
            return True
835

836 3
        text = "Needs to start with + and contain only numbers 0-9"
837

838 3
        match = re.match(r"(\+[0-9]+)", self.value)
839 3
        if match:
840 3
            match = self.value.replace(match.group(), "")
841 3
            if match:
842 3
                return text
843
        else:
844 3
            return text
845

846 3
        return True
847

848 3
    def formatHint(self):
849 3
        return "+1234567890"
850

851

852 3
class WebResourceURLDataType(WebURLDataType):
853 3
    def formatHint(self):
854 3
        return (
855
            "This resource may get downloaded and cached on the client "
856
            "computer. To ensure up-to-date resources, append a unique ID "
857
            "to the URL such as a timestamp of when the resources changed on your "
858
            "server, e.g. "
859
            "https://awesomefonts.com/xyz/regular/specimen.pdf?t=1548239062. "
860
            "Don’t use the current time for a timestamp, as this will mean constant "
861
            "reloading the resource when it actually hasn’t changed. Instead use "
862
            "the resource’s server-side change timestamp."
863
        )
864

865

866 3
class EmailDataType(StringDataType):
867 3
    def valid(self):
868 3
        if not self.value:
869 3
            return True
870 3
        if (
871
            "@" in self.value
872
            and "." in self.value
873
            and self.value.find(".", self.value.find("@")) > 0
874
            and self.value.count("@") == 1
875
            and self.value.find("..") == -1
876
        ):
877

878 3
            return True
879
        else:
880 3
            return "Not a valid email format: %s" % self.value
881

882

883 3
class HexColorDataType(StringDataType):
884 3
    def valid(self):
885 3
        if not self.value:
886 3
            return True
887 3
        if (len(self.value) == 3 or len(self.value) == 6) and re.match(
888
            "^[A-Fa-f0-9]*$", self.value
889
        ):
890

891 3
            return True
892
        else:
893 3
            return (
894
                "Not a valid hex color of format RRGGBB "
895
                "(like FF0000 for red): %s" % self.value
896
            )
897

898 3
    def formatHint(self):
899 3
        return "Hex RRGGBB (without leading #)"
900

901

902 3
class ListProxy(DataType):
903 3
    initialData = []
904 3
    includeEmpty = False
905

906
    # Data type of each list member
907
    # Here commented out to enforce explicit setting of data type
908
    # for each Proxy
909
    # dataType = str
910

911 3
    def __repr__(self):
912 3
        if self.value:
913 3
            return "%s" % ([x.get() for x in self.value])
914
        else:
915 3
            return "[]"
916

917 3
    def __getitem__(self, i):
918 3
        return self.value[i].get()
919

920 3
    def __setitem__(self, i, value):
921

922 3
        if issubclass(value.__class__, (DictBasedObject, Proxy, ListProxy, DataType)):
923 3
            object.__setattr__(value, "_parent", self)
924

925 3
        self.value[i].put(value)
926 3
        object.__setattr__(self.value[i], "_parent", self)
927

928 3
    def __delitem__(self, i):
929 3
        del self.value[i]
930

931 3
    def __iter__(self):
932 3
        for element in self.value:
933 3
            yield element.get()
934

935 3
    def __len__(self):
936 3
        return len(self.value)
937

938 3
    def index(self, item):
939 3
        return [x.get() for x in self.value].index(item)
940

941 3
    def get(self):
942 3
        return self
943

944 3
    def put(self, values):
945

946 3
        if not type(values) in (list, tuple):
947 3
            raise ValueError(
948
                "Wrong data type. Is %s, should be: %s." % (type(values), list)
949
            )
950

951 3
        self.value = []
952 3
        for value in values:
953 3
            self.append(value)
954

955 3
    def append(self, value):
956

957 3
        newData = self.dataType()
958 3
        newData.put(value)
959

960 3
        self.value.append(newData)
961

962 3
        if issubclass(newData.__class__, (DictBasedObject, Proxy, ListProxy, DataType)):
963 3
            object.__setattr__(newData, "_parent", self)
964

965 3
    def extend(self, values):
966 3
        for value in values:
967 3
            self.append(value)
968

969 3
    def remove(self, removeValue):
970 3
        for i, value in enumerate(self.value):
971 3
            if self[i] == removeValue:
972 3
                del self[i]
973

974 3
    def isEmpty(self):
975 3
        if self.includeEmpty:
976 3
            return False
977
        else:
978 3
            return not bool(self.value)
979

980
    # def valid(self):
981

982
    #     if self.value:
983
    #         for data in self.value:
984
    #             valid = data.valid()
985
    #             return valid
986
    #     return True
987

988

989 3
class DictBasedObject(object):
990 3
    _structure = {}
991 3
    _deprecatedKeys = []
992 3
    _possible_keys = []
993 3
    _dataType_for_possible_keys = None
994

995 3
    def __copy__(self):
996 3
        cls = self.__class__
997 3
        result = cls.__new__(cls)
998 3
        result.__dict__.update(self.__dict__)
999 3
        return result
1000

1001 3
    def __deepcopy__(self, memo):
1002 3
        cls = self.__class__
1003 3
        obj = cls()
1004 3
        obj.loadJSON(self.dumpJSON())
1005 3
        return obj
1006

1007 3
    def sameContent(self, other):
1008 3
        return self.difference(other) == {}
1009

1010 3
    def difference(self, other):
1011 3
        from deepdiff import DeepDiff
1012

1013 3
        return DeepDiff(self.dumpDict(), other.dumpDict(), ignore_order=True)
1014

1015 3
    def nonListProxyBasedKeys(self):
1016

1017 3
        _list = []
1018

1019 3
        for keyword in self._structure.keys():
1020 3
            if ListProxy not in inspect.getmro(self._structure[keyword][0]):
1021 3
                _list.append(keyword)
1022

1023 3
        _list.extend(self._deprecatedKeys)
1024

1025 3
        return _list
1026

1027 3
    def linkDocuText(self, text):
1028 3
        def my_replace(match):
1029 3
            match = match.group()
1030 3
            match = match[2:-2]
1031

1032 3
            if "." in match:
1033 3
                className, attributeName = match.split(".")
1034

1035 3
                if "()" in attributeName:
1036 3
                    attributeName = attributeName[:-2]
1037 3
                    match = "[%s.%s()](#user-content-class-%s-method-%s)" % (
1038
                        className,
1039
                        attributeName,
1040
                        className.lower(),
1041
                        attributeName.lower(),
1042
                    )
1043
                else:
1044 3
                    match = "[%s.%s](#user-content-class-%s-attribute-%s)" % (
1045
                        className,
1046
                        attributeName,
1047
                        className.lower(),
1048
                        attributeName.lower(),
1049
                    )
1050
            else:
1051 3
                className = match
1052 3
                match = "[%s](#user-content-class-%s)" % (
1053
                    className,
1054
                    className.lower(),
1055
                )
1056

1057 3
            return match
1058

1059 3
        try:
1060 3
            text = re.sub(r"::.+?::", my_replace, text)
1061 3
        except Exception:
1062 3
            pass
1063

1064 3
        return text or ""
1065

1066 3
    def typeDescription(self, class_):
1067

1068 3
        if issubclass(class_, ListProxy):
1069 3
            return "List of %s objects" % self.typeDescription(class_.dataType)
1070

1071 3
        elif class_.dataType in (
1072
            dict,
1073
            list,
1074
            tuple,
1075
            str,
1076
            bytes,
1077
            set,
1078
            frozenset,
1079
            bool,
1080
            int,
1081
            float,
1082
        ):
1083 3
            return class_.dataType.__name__.capitalize()
1084

1085 3
        elif "" in ("%s" % class_.dataType):
1086 3
            return self.linkDocuText("::%s::" % class_.dataType.__name__)
1087

1088
        # Seems unused
1089

1090
        # elif 'typeworld.api.' in ("%s" % class_.dataType):
1091
        #     return self.linkDocuText('::%s::' % class_.dataType.__name__)
1092

1093
        # else:
1094
        #     return class_.dataType.__name__.title()
1095

1096 3
    def additionalDocu(self):
1097

1098 3
        doc = ""
1099

1100 3
        if hasattr(self, "sample"):
1101

1102 3
            doc += f"""*Example JSON data:*
1103
```json
1104
{self.sample().dumpJSON(strict = False)}
1105
```
1106

1107
"""
1108

1109 3
        return doc
1110

1111 3
    def docu(self):
1112

1113 3
        classes = []
1114

1115
        # Define string
1116 3
        docstring = ""
1117 3
        head = ""
1118 3
        attributes = ""
1119 3
        methods = ""
1120 3
        attributesList = []
1121 3
        methodsList = []
1122

1123 3
        head += '<div id="class-%s"></div>\n\n' % self.__class__.__name__.lower()
1124

1125 3
        head += "# _class_ %s()\n\n" % self.__class__.__name__
1126

1127 3
        head += self.linkDocuText(inspect.getdoc(self))
1128

1129 3
        head += "\n\n"
1130

1131 3
        additionalDocu = self.additionalDocu()
1132 3
        if additionalDocu:
1133

1134 3
            head += additionalDocu + "\n\n"
1135

1136
        # attributes
1137

1138 3
        attributes += "## Attributes\n\n"
1139

1140 3
        for key in sorted(self._structure.keys()):
1141

1142 3
            attributesList.append(key)
1143 3
            attributes += '<div id="class-%s-attribute-%s"></div>\n\n' % (
1144
                self.__class__.__name__.lower(),
1145
                key,
1146
            )
1147 3
            attributes += "### %s\n\n" % key
1148

1149
            # Description
1150 3
            if self._structure[key][3]:
1151 3
                attributes += self.linkDocuText(self._structure[key][3]) + "\n\n"
1152

1153 3
            attributes += "__Required:__ %s" % self._structure[key][1] + "<br />\n"
1154

1155 3
            attributes += (
1156
                "__Type:__ %s" % self.typeDescription(self._structure[key][0])
1157
                + "<br />\n"
1158
            )
1159

1160
            # Format Hint
1161 3
            hint = self._structure[key][0]().formatHint()
1162 3
            if hint:
1163 3
                attributes += "__Format:__ %s" % hint + "<br />\n"
1164

1165 3
            if self._structure[key][2] is not None:
1166 3
                attributes += "__Default value:__ %s" % self._structure[key][2] + "\n\n"
1167

1168
            # Example Data
1169 3
            example = self._structure[key][0]().exampleData()
1170 3
            if example:
1171 3
                attributes += "Example:\n"
1172 3
                attributes += "```json\n"
1173 3
                attributes += json.dumps(example, indent=4)
1174 3
                attributes += "\n```\n"
1175

1176 3
        method_list = [
1177
            func
1178
            for func in dir(self)
1179
            if callable(getattr(self, func))
1180
            and not func.startswith("__")
1181
            and inspect.getdoc(getattr(self, func))
1182
        ]
1183

1184 3
        if method_list:
1185 3
            methods += "## Methods\n\n"
1186

1187 3
            for methodName in method_list:
1188

1189 3
                methodsList.append(methodName)
1190 3
                methods += '<div id="class-%s-method-%s"></div>\n\n' % (
1191
                    self.__class__.__name__.lower(),
1192
                    methodName.lower(),
1193
                )
1194

1195 3
                args = inspect.getfullargspec(getattr(self, methodName))
1196 3
                if args.args != ["self"]:
1197

1198 3
                    argList = []
1199 3
                    if args.args and args.defaults:
1200

1201 3
                        startPoint = len(args.args) - len(args.defaults)
1202 3
                        for i, defaultValue in enumerate(args.defaults):
1203 3
                            argList.append(
1204
                                "%s = %s" % (args.args[i + startPoint], defaultValue)
1205
                            )
1206

1207 3
                    methods += "#### %s(%s)\n\n" % (
1208
                        methodName,
1209
                        ", ".join(argList),
1210
                    )
1211
                else:
1212 3
                    methods += "#### %s()\n\n" % methodName
1213 3
                methods += (
1214
                    self.linkDocuText(inspect.getdoc(getattr(self, methodName)))
1215
                    + "\n\n"
1216
                )
1217

1218
        # Compile
1219 3
        docstring += head
1220

1221
        # TOC
1222 3
        if attributesList:
1223 3
            docstring += "### Attributes\n\n"
1224 3
            for attribute in attributesList:
1225 3
                docstring += "[%s](#class-%s-attribute-%s)<br />" % (
1226
                    attribute,
1227
                    self.__class__.__name__.lower(),
1228
                    attribute.lower(),
1229
                )
1230 3
            docstring += "\n\n"
1231

1232 3
        if methodsList:
1233 3
            docstring += "### Methods\n\n"
1234 3
            for methodName in methodsList:
1235 3
                docstring += "[%s()](#class-%s-method-%s)<br />" % (
1236
                    methodName,
1237
                    self.__class__.__name__.lower(),
1238
                    methodName.lower(),
1239
                )
1240 3
            docstring += "\n\n"
1241

1242 3
        if attributesList:
1243 3
            docstring += attributes
1244 3
            docstring += "\n\n"
1245

1246 3
        if methodsList:
1247 3
            docstring += methods
1248 3
            docstring += "\n\n"
1249

1250
        # Add data
1251 3
        classes.append([self.__class__.__name__, docstring])
1252

1253
        # Recurse
1254 3
        for key in list(self._structure.keys()):
1255 3
            if issubclass(self._structure[key][0], Proxy):
1256

1257 3
                o = self._structure[key][0].dataType()
1258 3
                classes.extend(o.docu())
1259

1260 3
            if issubclass(self._structure[key][0], ListProxy):
1261

1262 3
                o = self._structure[key][0].dataType.dataType()
1263 3
                if hasattr(o, "docu"):
1264 3
                    classes.extend(o.docu())
1265

1266 3
        return classes
1267

1268 3
    def __init__(self, json=None, dict=None):
1269

1270 3
        super(DictBasedObject, self).__init__()
1271

1272 3
        object.__setattr__(self, "_content", {})
1273 3
        object.__setattr__(
1274
            self,
1275
            "_allowedKeys",
1276
            set(self._structure.keys()) | set(self._possible_keys),
1277
        )
1278

1279
        # Fill default values
1280 3
        for key in self._structure:
1281

1282
            # Set default values
1283 3
            if self._structure[key][2] is not None:
1284 3
                setattr(self, key, self._structure[key][2])
1285

1286 3
        if json:
1287 3
            self.loadJSON(json)
1288 3
        elif dict:
1289 3
            self.loadDict(dict)
1290

1291 3
    def initAttr(self, key):
1292

1293 3
        if key not in self._content:
1294

1295 3
            if key in list(object.__getattribute__(self, "_structure").keys()):
1296 3
                self._content[key] = object.__getattribute__(self, "_structure")[key][
1297
                    0
1298
                ]()
1299

1300 3
            elif key in self._possible_keys:
1301 3
                self._content[key] = self._dataType_for_possible_keys()
1302

1303 3
            self._content[key]._parent = self
1304

1305 3
    def __getattr__(self, key):
1306

1307 3
        if key in self._allowedKeys:
1308 3
            self.initAttr(key)
1309 3
            return self._content[key].get()
1310

1311
        else:
1312 3
            return object.__getattribute__(self, key)
1313

1314 3
    def __setattr__(self, key, value):
1315

1316 3
        if key in self._allowedKeys:
1317 3
            self.initAttr(key)
1318

1319 3
            if issubclass(
1320
                value.__class__, (DictBasedObject, ListProxy, Proxy, DataType)
1321
            ):
1322 3
                object.__setattr__(value, "_parent", self)
1323

1324 3
            self.__dict__["_content"][key].put(value)
1325

1326
        else:
1327 3
            object.__setattr__(self, key, value)
1328

1329 3
    def set(self, key, value):
1330 3
        self.__setattr__(key, value)
1331

1332 3
    def get(self, key):
1333 3
        return self.__getattr__(key)
1334

1335 3
    def validate(self, strict=True):
1336

1337 3
        information = []
1338 3
        warnings = []
1339 3
        critical = []
1340

1341 3
        def extendWithKey(values, key=None, sourceObject=None):
1342

1343
            # Remove duplicates
1344 3
            seen = set()
1345 3
            seen_add = seen.add
1346 3
            values = [x for x in values if not (x in seen or seen_add(x))]
1347
            #                values = list(set(values))
1348

1349 3
            _list = []
1350 3
            for value in values:
1351 3
                if sourceObject and key:
1352 3
                    _list.append(
1353
                        "%s.%s --> %s --> %s" % (self, key, sourceObject, value)
1354
                    )
1355 3
                elif key:
1356 3
                    _list.append("%s.%s --> %s" % (self, key, value))
1357
                else:
1358 3
                    _list.append("%s --> %s" % (self, value))
1359 3
            return _list
1360

1361
        # Check if required fields are filled
1362 3
        for key in list(self._structure.keys()):
1363

1364 3
            self.initAttr(key)
1365

1366 3
            if self.discardThisKey(key) is False:
1367

1368 3
                if strict and self._structure[key][1] and self._content[key].isEmpty():
1369 3
                    critical.append(
1370
                        "%s.%s is a required attribute, but empty" % (self, key)
1371
                    )
1372

1373
                else:
1374

1375
                    # recurse
1376 3
                    if issubclass(self._content[key].__class__, (Proxy)):
1377

1378 3
                        if self._content[key].isEmpty() is False:
1379 3
                            (newInformation, newWarnings, newCritical,) = self._content[
1380
                                key
1381
                            ].value.validate(strict=strict)
1382 3
                            information.extend(extendWithKey(newInformation, key))
1383 3
                            warnings.extend(extendWithKey(newWarnings, key))
1384 3
                            critical.extend(extendWithKey(newCritical, key))
1385

1386
                            # Check custom messages:
1387 3
                            if hasattr(
1388
                                self._content[key].value, "customValidation"
1389
                            ) and isinstance(
1390
                                self._content[key].value.customValidation,
1391
                                types.MethodType,
1392
                            ):
1393 3
                                (
1394
                                    newInformation,
1395
                                    newWarnings,
1396
                                    newCritical,
1397
                                ) = self._content[key].value.customValidation()
1398 3
                                information.extend(
1399
                                    extendWithKey(
1400
                                        newInformation, key, self._content[key]
1401
                                    )
1402
                                )
1403 3
                                warnings.extend(
1404
                                    extendWithKey(newWarnings, key, self._content[key])
1405
                                )
1406 3
                                critical.extend(
1407
                                    extendWithKey(newCritical, key, self._content[key])
1408
                                )
1409

1410
                    # recurse
1411 3
                    if issubclass(self._content[key].__class__, (ListProxy)):
1412

1413 3
                        if self._content[key].isEmpty() is False:
1414 3
                            for item in self._content[key]:
1415 3
                                if hasattr(item, "validate") and isinstance(
1416
                                    item.validate, types.MethodType
1417
                                ):
1418 3
                                    (
1419
                                        newInformation,
1420
                                        newWarnings,
1421
                                        newCritical,
1422
                                    ) = item.validate(strict=strict)
1423 3
                                    information.extend(
1424
                                        extendWithKey(newInformation, key)
1425
                                    )
1426 3
                                    warnings.extend(extendWithKey(newWarnings, key))
1427 3
                                    critical.extend(extendWithKey(newCritical, key))
1428

1429
                                # Check custom messages:
1430 3
                                if hasattr(item, "customValidation") and isinstance(
1431
                                    item.customValidation, types.MethodType
1432
                                ):
1433 3
                                    (
1434
                                        newInformation,
1435
                                        newWarnings,
1436
                                        newCritical,
1437
                                    ) = item.customValidation()
1438 3
                                    information.extend(
1439
                                        extendWithKey(newInformation, key, item)
1440
                                    )
1441 3
                                    warnings.extend(
1442
                                        extendWithKey(newWarnings, key, item)
1443
                                    )
1444 3
                                    critical.extend(
1445
                                        extendWithKey(newCritical, key, item)
1446
                                    )
1447

1448
        # Check custom messages:
1449 3
        if (
1450
            issubclass(self.__class__, BaseResponse)
1451
            and hasattr(self, "customValidation")
1452
            and isinstance(self.customValidation, types.MethodType)
1453
        ):
1454 3
            newInformation, newWarnings, newCritical = self.customValidation()
1455 3
            information.extend(extendWithKey(newInformation))
1456 3
            warnings.extend(extendWithKey(newWarnings))
1457 3
            critical.extend(extendWithKey(newCritical))
1458

1459 3
        return information, warnings, critical
1460

1461 3
    def discardThisKey(self, key):
1462 3
        return False
1463

1464 3
    def dumpDict(self, strict=True, validate=True):
1465

1466 3
        d = {}
1467

1468
        # Auto-validate
1469 3
        if validate:
1470 3
            information, warnings, critical = self.validate(strict=strict)
1471 3
            if critical:
1472 3
                raise ValueError(critical[0])
1473

1474 3
        for key in list(self._content.keys()):
1475

1476 3
            if self.discardThisKey(key) is False:
1477

1478 3
                if (
1479
                    # required
1480
                    (key in self._structure and self._structure[key][1])
1481
                    # don't know
1482
                    or getattr(self, key)
1483
                    # is set
1484
                    or (
1485
                        hasattr(getattr(self, key), "isSet")
1486
                        and getattr(self, key).isSet()
1487
                    )
1488
                ):
1489

1490 3
                    if hasattr(getattr(self, key), "dumpDict"):
1491 3
                        d[key] = getattr(self, key).dumpDict(
1492
                            strict=strict, validate=validate
1493
                        )
1494

1495 3
                    elif issubclass(getattr(self, key).__class__, (ListProxy)):
1496 3
                        d[key] = list(getattr(self, key))
1497

1498 3
                        if len(d[key]) > 0 and hasattr(d[key][0], "dumpDict"):
1499 3
                            d[key] = [
1500
                                x.dumpDict(strict=strict, validate=validate)
1501
                                for x in d[key]
1502
                            ]
1503

1504
                    else:
1505 3
                        d[key] = getattr(self, key)
1506

1507 3
        return d
1508

1509 3
    def loadDict(self, d):
1510

1511 3
        for key in d:
1512 3
            if key in self._allowedKeys:
1513

1514 3
                if key in self._structure:
1515

1516 3
                    if issubclass(self._structure[key][0], (Proxy)):
1517

1518 3
                        try:
1519 3
                            exec(
1520
                                "self.%s = typeworld.api.%s()"
1521
                                % (
1522
                                    key,
1523
                                    self._structure[key][0].dataType.__name__,
1524
                                )
1525
                            )
1526 3
                        except Exception:
1527 3
                            exec(
1528
                                "self.%s = %s()"
1529
                                % (
1530
                                    key,
1531
                                    self._structure[key][0].dataType.__name__,
1532
                                )
1533
                            )
1534 3
                        exec("self.%s.loadDict(d[key])" % (key))
1535

1536 3
                    elif issubclass(self._structure[key][0], (ListProxy)):
1537 3
                        _list = self.__getattr__(key)
1538 3
                        _list.value = []
1539
                        # allow empty
1540
                        # if self._structure[key][0].includeEmpty:
1541
                        #     _list.value = []
1542 3
                        for item in d[key]:
1543 3
                            o = self._structure[key][0].dataType.dataType()
1544

1545 3
                            if hasattr(o, "loadDict"):
1546 3
                                o.loadDict(item)
1547 3
                                _list.append(o)
1548
                            else:
1549 3
                                _list.append(item)
1550 3
                        exec("self._content[key] = _list")
1551

1552
                    else:
1553 3
                        self.set(key, d[key])
1554

1555 3
    def dumpJSON(self, strict=True):
1556 3
        return json.dumps(self.dumpDict(strict=strict), indent=4, sort_keys=True)
1557

1558 3
    def loadJSON(self, j):
1559 3
        self.loadDict(json.loads(j))
1560

1561

1562 3
class Proxy(DataType):
1563 3
    pass
1564

1565

1566 3
class ResponseCommandDataType(StringDataType):
1567 3
    def formatHint(self):
1568 3
        return (
1569
            "To ensure the proper function of the entire Type.World protocol, "
1570
            "your API endpoint *must* return the proper responses as per "
1571
            "[this flow chart](https://type.world/documentation/Type.World%20"
1572
            "Request%20Flow%20Chart.pdf). "
1573
            "In addition to ensure functionality, this enables the response "
1574
            "messages displayed to the user to be translated into all the "
1575
            "possible languages on our side."
1576
        )
1577

1578

1579 3
class MultiLanguageText(DictBasedObject):
1580
    """\
1581
Multi-language text. Attributes are language keys as per
1582
[https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes]
1583

1584
The GUI app will then calculate the language data to be displayed using
1585
::MultiLanguageText.getText():: with a prioritized list of languages that
1586
the user can understand. They may be pulled from the operating system’s
1587
language preferences.
1588

1589
These classes are already initiated wherever they are used, and can be
1590
addresses instantly with the language attributes:
1591

1592
```python
1593
api.name.en = u'Font Publisher XYZ'
1594
api.name.de = u'Schriftenhaus XYZ'
1595
```
1596

1597
If you are loading language information from an external source, you may use
1598
the `.set()` method to enter data:
1599

1600
```python
1601
# Simulating external data source
1602
for languageCode, text in (
1603
        ('en': u'Font Publisher XYZ'),
1604
        ('de': u'Schriftenhaus XYZ'),
1605
    )
1606
    api.name.set(languageCode, text)
1607
```
1608

1609
Neither HTML nor Markdown code is permitted in `MultiLanguageText`.
1610
"""
1611

1612 3
    _possible_keys = [
1613
        "ab",
1614
        "aa",
1615
        "af",
1616
        "ak",
1617
        "sq",
1618
        "am",
1619
        "ar",
1620
        "an",
1621
        "hy",
1622
        "as",
1623
        "av",
1624
        "ae",
1625
        "ay",
1626
        "az",
1627
        "bm",
1628
        "ba",
1629
        "eu",
1630
        "be",
1631
        "bn",
1632
        "bh",
1633
        "bi",
1634
        "bs",
1635
        "br",
1636
        "bg",
1637
        "my",
1638
        "ca",
1639
        "ch",
1640
        "ce",
1641
        "ny",
1642
        "zh",
1643
        "cv",
1644
        "kw",
1645
        "co",
1646
        "cr",
1647
        "hr",
1648
        "cs",
1649
        "da",
1650
        "dv",
1651
        "nl",
1652
        "dz",
1653
        "en",
1654
        "eo",
1655
        "et",
1656
        "ee",
1657
        "fo",
1658
        "fj",
1659
        "fi",
1660
        "fr",
1661
        "ff",
1662
        "gl",
1663
        "ka",
1664
        "de",
1665
        "el",
1666
        "gn",
1667
        "gu",
1668
        "ht",
1669
        "ha",
1670
        "he",
1671
        "hz",
1672
        "hi",
1673
        "ho",
1674
        "hu",
1675
        "ia",
1676
        "id",
1677
        "ie",
1678
        "ga",
1679
        "ig",
1680
        "ik",
1681
        "io",
1682
        "is",
1683
        "it",
1684
        "iu",
1685
        "ja",
1686
        "jv",
1687
        "kl",
1688
        "kn",
1689
        "kr",
1690
        "ks",
1691
        "kk",
1692
        "km",
1693
        "ki",
1694
        "rw",
1695
        "ky",
1696
        "kv",
1697
        "kg",
1698
        "ko",
1699
        "ku",
1700
        "kj",
1701
        "la",
1702
        "lb",
1703
        "lg",
1704
        "li",
1705
        "ln",
1706
        "lo",
1707
        "lt",
1708
        "lu",
1709
        "lv",
1710
        "gv",
1711
        "mk",
1712
        "mg",
1713
        "ms",
1714
        "ml",
1715
        "mt",
1716
        "mi",
1717
        "mr",
1718
        "mh",
1719
        "mn",
1720
        "na",
1721
        "nv",
1722
        "nd",
1723
        "ne",
1724
        "ng",
1725
        "nb",
1726
        "nn",
1727
        "no",
1728
        "ii",
1729
        "nr",
1730
        "oc",
1731
        "oj",
1732
        "cu",
1733
        "om",
1734
        "or",
1735
        "os",
1736
        "pa",
1737
        "pi",
1738
        "fa",
1739
        "pl",
1740
        "ps",
1741
        "pt",
1742
        "qu",
1743
        "rm",
1744
        "rn",
1745
        "ro",
1746
        "ru",
1747
        "sa",
1748
        "sc",
1749
        "sd",
1750
        "se",
1751
        "sm",
1752
        "sg",
1753
        "sr",
1754
        "gd",
1755
        "sn",
1756
        "si",
1757
        "sk",
1758
        "sl",
1759
        "so",
1760
        "st",
1761
        "es",
1762
        "su",
1763
        "sw",
1764
        "ss",
1765
        "sv",
1766
        "ta",
1767
        "te",
1768
        "tg",
1769
        "th",
1770
        "ti",
1771
        "bo",
1772
        "tk",
1773
        "tl",
1774
        "tn",
1775
        "to",
1776
        "tr",
1777
        "ts",
1778
        "tt",
1779
        "tw",
1780
        "ty",
1781
        "ug",
1782
        "uk",
1783
        "ur",
1784
        "uz",
1785
        "ve",
1786
        "vi",
1787
        "vo",
1788
        "wa",
1789
        "cy",
1790
        "wo",
1791
        "fy",
1792
        "xh",
1793
        "yi",
1794
        "yo",
1795
        "za",
1796
        "zu",
1797
    ]
1798 3
    _dataType_for_possible_keys = StringDataType
1799 3
    _length = 100
1800 3
    _markdownAllowed = False
1801

1802
    # def __repr__(self):
1803
    #     return '<MultiLanguageText>'
1804

1805 3
    def __str__(self):
1806 3
        return str(self.getText())
1807

1808 3
    def __bool__(self):
1809 3
        return bool(self.getText())
1810

1811 3
    def sample(self):
1812 3
        o = self.__class__()
1813 3
        o.en = "Text in English"
1814 3
        o.de = "Text auf Deutsch"
1815 3
        return o
1816

1817 3
    def getTextAndLocale(self, locale=["en"]):
1818
        """Like getText(), but additionally returns the language of whatever
1819
        text was found first."""
1820

1821 3
        if type(locale) in (str, str):
1822 3
            if self.get(locale):
1823 3
                return self.get(locale), locale
1824

1825 3
        elif type(locale) in (list, tuple):
1826 3
            for key in locale:
1827 3
                if self.get(key):
1828 3
                    return self.get(key), key
1829

1830
        # try english
1831 3
        if self.get("en"):
1832 3
            return self.get("en"), "en"
1833

1834
        # try anything
1835 3
        for key in self._possible_keys:
1836 3
            if self.get(key):
1837 3
                return self.get(key), key
1838

1839 3
        return None, None
1840

1841 3
    def getText(self, locale=["en"]):
1842
        """Returns the text in the first language found from the specified
1843
        list of languages. If that language can’t be found, we’ll try English
1844
        as a standard. If that can’t be found either, return the first language
1845
        you can find."""
1846

1847 3
        text, locale = self.getTextAndLocale(locale)
1848

1849 3
        return text
1850

1851 3
    def customValidation(self):
1852

1853 3
        information, warnings, critical = [], [], []
1854

1855 3
        if self.isEmpty():
1856 3
            critical.append("Needs to contain at least one language field")
1857

1858
        # Check for text length
1859 3
        for langId in self._possible_keys:
1860 3
            if self.get(langId):
1861 3
                string = self.get(langId)
1862 3
                if len(string) > self._length:
1863 3
                    critical.append(
1864
                        (
1865
                            "Language entry '%s' is too long. "
1866
                            "Allowed are %s characters."
1867
                        )
1868
                        % (langId, self._length)
1869
                    )
1870

1871 3
                if re.findall(r"(<.+?>)", string):
1872 3
                    if self._markdownAllowed:
1873 3
                        critical.append(
1874
                            (
1875
                                "String contains HTML code, which is not "
1876
                                "allowed. You may use Markdown for text "
1877
                                "formatting. String: " + string
1878
                            )
1879
                        )
1880
                    else:
1881 3
                        critical.append(
1882
                            "String contains HTML code, which is not allowed. String: "
1883
                            + string
1884
                        )
1885

1886 3
                if (
1887
                    not self._markdownAllowed
1888
                    and string
1889
                    and "<p>" + string + "</p>\n" != markdown2.markdown(string)
1890
                ):
1891 3
                    critical.append(
1892
                        "String contains Markdown code, which is not allowed."
1893
                    )
1894

1895 3
        return information, warnings, critical
1896

1897 3
    def isSet(self):
1898 3
        return not self.isEmpty()
1899

1900 3
    def isEmpty(self):
1901

1902
        # Check for existence of languages
1903 3
        hasAtLeastOneLanguage = False
1904 3
        for langId in self._possible_keys:
1905 3
            if langId in self._content and self.getText([langId]) is not None:
1906 3
                hasAtLeastOneLanguage = True
1907 3
                break
1908

1909 3
        return not hasAtLeastOneLanguage
1910

1911 3
    def loadDict(self, d):
1912 3
        for key in d:
1913 3
            self.set(key, d[key])
1914

1915

1916 3
def MultiLanguageText_Parent(self):
1917 3
    if hasattr(self, "_parent") and hasattr(self._parent, "_parent"):
1918 3
        return self._parent._parent
1919

1920

1921 3
MultiLanguageText.parent = property(lambda self: MultiLanguageText_Parent(self))
1922

1923

1924 3
class MultiLanguageTextProxy(Proxy):
1925 3
    dataType = MultiLanguageText
1926

1927 3
    def isEmpty(self):
1928 3
        return self.value.isEmpty()
1929

1930 3
    def formatHint(self):
1931 3
        text = "Maximum allowed characters: %s." % self.dataType._length
1932 3
        if self.dataType._markdownAllowed:
1933 3
            text += " Mardown code is permitted for text formatting."
1934 3
        return text
1935

1936

1937 3
class MultiLanguageTextListProxy(ListProxy):
1938 3
    dataType = MultiLanguageTextProxy
1939

1940

1941
###############################################################################
1942

1943

1944 3
class MultiLanguageLongText(MultiLanguageText):
1945
    """\
1946
Multi-language text. Attributes are language keys as per
1947
[https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes]
1948

1949
The GUI app will then calculate the language data to be displayed using
1950
::MultiLanguageText.getText():: with a prioritized list of languages that
1951
the user can understand. They may be pulled from the operating system’s
1952
language preferences.
1953

1954
These classes are already initiated wherever they are used, and can be
1955
addresses instantly with the language attributes:
1956

1957
```python
1958
api.name.en = u'Font Publisher XYZ'
1959
api.name.de = u'Schriftenhaus XYZ'
1960
```
1961

1962
If you are loading language information from an external source, you may use
1963
the `.set()` method to enter data:
1964

1965
```python
1966
# Simulating external data source
1967
for languageCode, text in (
1968
        ('en': u'Font Publisher XYZ'),
1969
        ('de': u'Schriftenhaus XYZ'),
1970
    )
1971
    api.name.set(languageCode, text)
1972
```
1973

1974
Neither HTML nor Markdown code is permitted in `MultiLanguageText`.
1975
"""
1976

1977 3
    _length = 3000
1978 3
    _markdownAllowed = True
1979

1980

1981 3
class MultiLanguageLongTextProxy(MultiLanguageTextProxy):
1982 3
    dataType = MultiLanguageLongText
1983

1984

1985
###############################################################################
1986
###############################################################################
1987
###############################################################################
1988
###############################################################################
1989
###############################################################################
1990
###############################################################################
1991

1992
#  Top-Level Data Types
1993

1994

1995 3
class LanguageSupportDataType(DictionaryDataType):
1996 3
    def valid(self):
1997 3
        if not self.value:
1998 3
            return True
1999 3
        for script in self.value:
2000 3
            if not len(script) == 4 or not script.islower():
2001 3
                return "Script tag '%s' needs to be a four-letter lowercase tag." % (
2002
                    script
2003
                )
2004

2005 3
            for language in self.value[script]:
2006 3
                if not len(language) == 3 or not language.isupper():
2007 3
                    return (
2008
                        "Language tag '%s' needs to be a " "three-letter uppercase tag."
2009
                    ) % (language)
2010

2011 3
        return True
2012

2013

2014 3
class OpenTypeFeatureDataType(StringDataType):
2015 3
    def valid(self):
2016 3
        if not self.value:
2017 3
            return True
2018

2019 3
        if not len(self.value) == 4 or not self.value.islower():
2020 3
            return (
2021
                "OpenType feature tag '%s' needs to be a " "four-letter lowercase tag."
2022
            ) % (self.value)
2023

2024 3
        return True
2025

2026

2027 3
class OpenTypeFeatureListProxy(ListProxy):
2028 3
    dataType = OpenTypeFeatureDataType
2029

2030

2031 3
class OpenSourceLicenseIdentifierDataType(StringDataType):
2032 3
    def valid(self):
2033 3
        if not self.value:
2034 3
            return True
2035

2036 3
        if self.value in OPENSOURCELICENSES:
2037 3
            return True
2038
        else:
2039 3
            return (
2040
                "Unknown license identifier: '%s'. " "See https://spdx.org/licenses/"
2041
            ) % (self.value)
2042

2043

2044 3
class SupportedAPICommandsDataType(StringDataType):
2045

2046 3
    commands = [x["keyword"] for x in COMMANDS]
2047

2048 3
    def valid(self):
2049 3
        if not self.value:
2050 3
            return True
2051

2052 3
        if self.value in self.commands:
2053 3
            return True
2054
        else:
2055 3
            return "Unknown API command: '%s'. Possible: %s" % (
2056
                self.value,
2057
                self.commands,
2058
            )
2059

2060

2061 3
class SupportedAPICommandsListProxy(ListProxy):
2062 3
    dataType = SupportedAPICommandsDataType
2063

2064

2065 3
class SupportedPublisherTypeDataType(StringDataType):
2066

2067 3
    types = PUBLISHERTYPES
2068

2069 3
    def valid(self):
2070 3
        if not self.value:
2071 0
            return True
2072

2073 3
        if self.value in self.types:
2074 3
            return True
2075
        else:
2076 3
            return "Unknown publisher type: '%s'. Possible: %s" % (
2077
                self.value,
2078
                self.types,
2079
            )
2080

2081

2082 3
class SupportedPublisherTypeListProxy(ListProxy):
2083 3
    dataType = SupportedPublisherTypeDataType
2084

2085

2086 3
class FontPurposeDataType(StringDataType):
2087 3
    def valid(self):
2088 3
        if not self.value:
2089 3
            return True
2090

2091 3
        if self.value in list(FONTPURPOSES.keys()):
2092 3
            return True
2093
        else:
2094 3
            return "Unknown font type: '%s'. Possible: %s" % (
2095
                self.value,
2096
                list(FONTPURPOSES.keys()),
2097
            )
2098

2099

2100 3
class FontMimeType(StringDataType):
2101 3
    def valid(self):
2102 3
        if not self.value:
2103 3
            return True
2104

2105 3
        if self.value in list(FONTPURPOSES["desktop"]["acceptableMimeTypes"]):
2106 3
            return True
2107
        else:
2108 3
            return "Unknown font MIME Type: '%s'. Possible: %s" % (
2109
                self.value,
2110
                list(FONTPURPOSES["desktop"]["acceptableMimeTypes"]),
2111
            )
2112

2113

2114 3
class FontStatusDataType(StringDataType):
2115

2116 3
    statuses = FONTSTATUSES
2117

2118 3
    def valid(self):
2119 3
        if not self.value:
2120 3
            return True
2121

2122 3
        if self.value in self.statuses:
2123 3
            return True
2124
        else:
2125 3
            return "Unknown Font Status: '%s'. Possible: %s" % (
2126
                self.value,
2127
                self.statuses,
2128
            )
2129

2130

2131 3
class FontExtensionDataType(StringDataType):
2132 3
    def valid(self):
2133 3
        if not self.value:
2134 3
            return True
2135

2136 3
        found = False
2137

2138 3
        for mimeType in list(MIMETYPES.keys()):
2139 3
            if self.value in MIMETYPES[mimeType]["fileExtensions"]:
2140 3
                found = True
2141 3
                break
2142

2143 3
        if found:
2144 3
            return True
2145
        else:
2146

2147 3
            return "Unknown font extension: '%s'. Possible: %s" % (
2148
                self.value,
2149
                FILEEXTENSIONS,
2150
            )
2151

2152

2153
###############################################################################
2154

2155
#  LicenseDefinition
2156

2157

2158 3
class LicenseDefinition(DictBasedObject):
2159
    #   key:  [data type, required, default value, description]
2160 3
    _structure = {
2161
        "keyword": [
2162
            StringDataType,
2163
            True,
2164
            None,
2165
            (
2166
                "Machine-readable keyword under which the license will be "
2167
                "referenced from the individual fonts."
2168
            ),
2169
        ],
2170
        "name": [
2171
            MultiLanguageTextProxy,
2172
            True,
2173
            None,
2174
            "Human-readable name of font license",
2175
        ],
2176
        "URL": [
2177
            WebURLDataType,
2178
            True,
2179
            None,
2180
            "URL where the font license text can be viewed online",
2181
        ],
2182
    }
2183

2184 3
    def __repr__(self):
2185 3
        return "<LicenseDefinition '%s'>" % self.name or self.keyword or "undefined"
2186

2187 3
    def sample(self):
2188 3
        o = self.__class__()
2189 3
        o.keyword = "awesomefontsEULA"
2190 3
        o.name.en = "Awesome Fonts End User License Agreement"
2191 3
        o.name.de = "Awesome Fonts Endnutzerlizenzvereinbarung"
2192 3
        o.URL = "https://awesomefonts.com/eula.html"
2193 3
        return o
2194

2195

2196 3
def LicenseDefinition_Parent(self):
2197 3
    if (
2198
        hasattr(self, "_parent")
2199
        and hasattr(self._parent, "_parent")
2200
        and hasattr(self._parent._parent, "_parent")
2201
    ):
2202 3
        return self._parent._parent._parent
2203

2204

2205 3
LicenseDefinition.parent = property(lambda self: LicenseDefinition_Parent(self))
2206

2207

2208 3
class LicenseDefinitionProxy(Proxy):
2209 3
    dataType = LicenseDefinition
2210

2211

2212 3
class LicenseDefinitionListProxy(ListProxy):
2213 3
    dataType = LicenseDefinitionProxy
2214

2215

2216
###############################################################################
2217

2218
#  FontPackage
2219

2220

2221 3
class FontPackage(DictBasedObject):
2222
    """\
2223
    `FontPackages` are groups of fonts that serve a certain purpose
2224
    to the user.
2225
    They can be defined at ::InstallableFontsReponse.packages::,
2226
    ::Foundry.packages::, ::Family.packages::
2227
    and are referenced by their keywords in ::Font.packageKeywords::.
2228

2229
    On a font family level, defined at ::Family.packages::, a typical example
2230
    for defining a `FontPackage` would be the so called **Office Fonts**.
2231
    While they are technically identical to other OpenType fonts, they normally
2232
    have a sightly different set of glyphs and OpenType features.
2233
    Linking them to a `FontPackage` allows the UI to display them clearly as a
2234
    separate set of fonts that serve a different purpuse than the
2235
    regular fonts.
2236

2237
    On a subscription-wide level, defined at
2238
    ::InstallableFontsReponse.packages::, a `FontPackage` could represent a
2239
    curated collection of fonts of various foundries and families, for example
2240
    **Script Fonts** or **Brush Fonts** or **Corporate Fonts**.
2241

2242
    Each font may be part of several `FontPackages`.
2243

2244
    For the time being, only family-level FontPackages are supported in the UI.
2245
    """
2246

2247
    #   key:  [data type, required, default value, description]
2248 3
    _structure = {
2249
        "keyword": [
2250
            StringDataType,
2251
            True,
2252
            None,
2253
            (
2254
                "Keyword of font packages. This keyword must be referenced in "
2255
                "::Font.packageKeywords:: and must be unique to this subscription."
2256
            ),
2257
        ],
2258
        "name": [MultiLanguageTextProxy, True, None, "Name of package"],
2259
        "description": [MultiLanguageTextProxy, False, None, "Description"],
2260
    }
2261

2262 3
    def __repr__(self):
2263 3
        return "<FontPackage '%s'>" % self.keyword or "undefined"
2264

2265 3
    def sample(self):
2266 3
        o = self.__class__()
2267 3
        o.keyword = "officefonts"
2268 3
        o.name.en = "Office Fonts"
2269 3
        o.name.de = "Office-Schriften"
2270 3
        o.description.en = (
2271
            "These fonts are produced specifically to be used in "
2272
            "Office applications."
2273
        )
2274 3
        o.description.de = (
2275
            "Diese Schriftdateien sind für die Benutzung in "
2276
            "Office-Applikationen vorgesehen."
2277
        )
2278 3
        return o
2279

2280 3
    def getFonts(self, filterByFontFormat=[], variableFont=None):
2281
        """
2282
        Calculate list of fonts of this package by applying filters for
2283
        font.format and font.variableFont (possibly more in the future)
2284
        """
2285

2286 0
        def passedFilter(font):
2287
            # font.format filter
2288 0
            passed1 = not filterByFontFormat or (
2289
                filterByFontFormat and font.format in filterByFontFormat
2290
            )
2291
            # font.variableFont filter
2292 0
            passed2 = variableFont is None or (
2293
                variableFont is not None and font.variableFont == variableFont
2294
            )
2295 0
            return passed1 and passed2
2296

2297 0
        return [x for x in self.fonts if passedFilter(x)]
2298

2299 3
    def getFormats(self):
2300 3
        formats = []
2301 3
        if hasattr(self, "fonts"):
2302 3
            for font in self.fonts:
2303 3
                if font.format not in formats:
2304 3
                    formats.append(font.format)
2305

2306 3
        return formats
2307

2308

2309 3
class FontPackageProxy(Proxy):
2310 3
    dataType = FontPackage
2311

2312

2313 3
class FontPackageListProxy(ListProxy):
2314 3
    dataType = FontPackageProxy
2315

2316

2317 3
class FontPackageReferencesListProxy(ListProxy):
2318 3
    dataType = StringDataType
2319

2320

2321
###############################################################################
2322

2323
#  LicenseUsage
2324

2325

2326 3
class LicenseUsage(DictBasedObject):
2327
    #   key:  [data type, required, default value, description]
2328 3
    _structure = {
2329
        "keyword": [
2330
            StringDataType,
2331
            True,
2332
            None,
2333
            (
2334
                "Keyword reference of font’s license. This license must be "
2335
                "specified in ::Foundry.licenses::"
2336
            ),
2337
        ],
2338
        "seatsAllowed": [
2339
            IntegerDataType,
2340
            False,
2341
            0,
2342
            (
2343
                "In case of desktop font (see ::Font.purpose::), number of "
2344
                "installations permitted by the user’s license."
2345
            ),
2346
        ],
2347
        "seatsInstalled": [
2348
            IntegerDataType,
2349
            False,
2350
            0,
2351
            (
2352
                "In case of desktop font (see ::Font.purpose::), number of "
2353
                "installations recorded by the API endpoint. This value will "
2354
                "need to be supplied dynamically by the API endpoint through "
2355
                "tracking all font installations through the `anonymousAppID` "
2356
                "parameter of the '%s' and '%s' command. Please note that the "
2357
                "Type.World client app is currently not designed to reject "
2358
                "installations of the fonts when the limits are exceeded. "
2359
                "Instead it is in the responsibility of the API endpoint to "
2360
                "reject font installations though the '%s' command when the "
2361
                "limits are exceeded. In that case the user will be presented "
2362
                "with one or more license upgrade links."
2363
            )
2364
            % (
2365
                INSTALLFONTSCOMMAND["keyword"],
2366
                UNINSTALLFONTSCOMMAND["keyword"],
2367
                INSTALLFONTSCOMMAND["keyword"],
2368
            ),
2369
        ],
2370
        "allowanceDescription": [
2371
            MultiLanguageTextProxy,
2372
            False,
2373
            None,
2374
            (
2375
                "In case of non-desktop font (see ::Font.purpose::), custom "
2376
                "string for web fonts or app fonts reminding the user of the "
2377
                "license’s limits, e.g. '100.000 page views/month'"
2378
            ),
2379
        ],
2380
        "upgradeURL": [
2381
            WebURLDataType,
2382
            False,
2383
            None,
2384
            (
2385
                "URL the user can be sent to to upgrade the license of the "
2386
                "font, for instance at the foundry’s online shop. If "
2387
                "possible, this link should be user-specific and guide "
2388
                "him/her as far into the upgrade process as possible."
2389
            ),
2390
        ],
2391
        "dateAddedForUser": [
2392
            DateDataType,
2393
            False,
2394
            None,
2395
            (
2396
                "Date that the user has purchased this font or the font has "
2397
                "become available to the user otherwise (like a new font "
2398
                "within a foundry’s beta font repository). Will be used in "
2399
                "the UI to signal which fonts have become newly available "
2400
                "in addition to previously available fonts. This is not to "
2401
                "be confused with the ::Version.releaseDate::, although they "
2402
                "could be identical."
2403
            ),
2404
        ],
2405
    }
2406

2407 3
    def sample(self):
2408 3
        o = self.__class__()
2409 3
        o.keyword = "awesomefontsEULA"
2410 3
        o.seatsAllowed = 5
2411 3
        o.seatsInstalled = 2
2412 3
        o.upgradeURL = "https://awesomefonts.com/shop/upgradelicense/083487263904356"
2413 3
        return o
2414

2415 3
    def __repr__(self):
2416 3
        return "<LicenseUsage '%s'>" % self.keyword or "undefined"
2417

2418 3
    def customValidation(self):
2419 3
        information, warnings, critical = [], [], []
2420

2421
        # Checking for existing license
2422 3
        if self.keyword and not self.getLicense():
2423 3
            critical.append(
2424
                "Has license '%s', but %s has no matching license."
2425
                % (self.keyword, self.parent.parent.parent)
2426
            )
2427

2428 3
        return information, warnings, critical
2429

2430 3
    def getLicense(self):
2431
        """\
2432
        Returns the ::License:: object that this font references.
2433
        """
2434 3
        return self.parent.parent.parent.getLicenseByKeyword(self.keyword)
2435

2436

2437 3
def LicenseUsage_Parent(self):
2438 3
    if (
2439
        hasattr(self, "_parent")
2440
        and hasattr(self._parent, "_parent")
2441
        and hasattr(self._parent._parent, "_parent")
2442
    ):
2443 3
        return self._parent._parent._parent
2444

2445

2446 3
LicenseUsage.parent = property(lambda self: LicenseUsage_Parent(self))
2447

2448

2449 3
class LicenseUsageProxy(Proxy):
2450 3
    dataType = LicenseUsage
2451

2452

2453 3
class LicenseUsageListProxy(ListProxy):
2454 3
    dataType = LicenseUsageProxy
2455

2456

2457
#######################################################################################
2458

2459
#  Designer
2460

2461

2462 3
class Designer(DictBasedObject):
2463
    #   key:                    [data type, required, default value, description]
2464 3
    _structure = {
2465
        "keyword": [
2466
            StringDataType,
2467
            True,
2468
            None,
2469
            (
2470
                "Machine-readable keyword under which the designer will be referenced "
2471
                "from the individual fonts or font families"
2472
            ),
2473
        ],
2474
        "name": [
2475
            MultiLanguageTextProxy,
2476
            True,
2477
            None,
2478
            "Human-readable name of designer",
2479
        ],
2480
        "websiteURL": [WebURLDataType, False, None, "Designer’s web site"],
2481
        "description": [
2482
            MultiLanguageLongTextProxy,
2483
            False,
2484
            None,
2485
            "Description of designer",
2486
        ],
2487
    }
2488

2489 3
    def sample(self):
2490 3
        o = self.__class__()
2491 3
        o.keyword = "johndoe"
2492 3
        o.name.en = "John Doe"
2493 3
        o.websiteURL = "https://johndoe.com"
2494 3
        return o
2495

2496 3
    def __repr__(self):
2497 3
        return "<Designer '%s'>" % self.name.getText() or self.keyword or "undefined"
2498

2499

2500 3
def Designer_Parent(self):
2501 3
    if (
2502
        hasattr(self, "_parent")
2503
        and hasattr(self._parent, "_parent")
2504
        and hasattr(self._parent._parent, "_parent")
2505
    ):
2506 3
        return self._parent._parent._parent
2507

2508

2509 3
Designer.parent = property(lambda self: Designer_Parent(self))
2510

2511

2512 3
class DesignerProxy(Proxy):
2513 3
    dataType = Designer
2514

2515

2516 3
class DesignersListProxy(ListProxy):
2517 3
    dataType = DesignerProxy
2518

2519

2520 3
class DesignersReferencesListProxy(ListProxy):
2521 3
    dataType = StringDataType
2522

2523

2524
########################################################################################
2525

2526
#  Font Family Version
2527

2528

2529 3
class Version(DictBasedObject):
2530
    #   key:                    [data type, required, default value, description]
2531 3
    _structure = {
2532
        "number": [
2533
            VersionDataType,
2534
            True,
2535
            None,
2536
            (
2537
                "Font version number. This can be a simple float number (1.002) or a "
2538
                "semver version string (see https://semver.org). For comparison, "
2539
                "single-dot version numbers (or even integers) are appended with "
2540
                "another .0 (1.0 to 1.0.0), then compared using the Python `semver` "
2541
                "module."
2542
            ),
2543
        ],
2544
        "description": [
2545
            MultiLanguageLongTextProxy,
2546
            False,
2547
            None,
2548
            "Description of font version",
2549
        ],
2550
        "releaseDate": [DateDataType, False, None, "Font version’s release date."],
2551
    }
2552

2553 3
    def sample(self):
2554 3
        o = self.__class__()
2555 3
        o.number = "1.2"
2556 3
        o.description.en = "Added capital SZ and Turkish Lira sign"
2557 3
        o.description.de = "Versal-SZ und türkisches Lira-Zeichen hinzugefügt"
2558 3
        o.releaseDate = "2020-05-21"
2559 3
        return o
2560

2561 3
    def __repr__(self):
2562 3
        return "<Version %s (%s)>" % (
2563
            self.number if self.number else "None",
2564
            "font-specific" if self.isFontSpecific() else "family-specific",
2565
        )
2566

2567 3
    def isFontSpecific(self):
2568
        """\
2569
        Returns True if this version is defined at the font level.
2570
        Returns False if this version is defined at the family level.
2571
        """
2572 3
        return issubclass(self.parent.__class__, Font)
2573

2574

2575 3
def Version_Parent(self):
2576 3
    if (
2577
        hasattr(self, "_parent")
2578
        and hasattr(self._parent, "_parent")
2579
        and hasattr(self._parent._parent, "_parent")
2580
    ):
2581 3
        return self._parent._parent._parent
2582

2583

2584 3
Version.parent = property(lambda self: Version_Parent(self))
2585

2586

2587 3
class VersionProxy(Proxy):
2588 3
    dataType = Version
2589

2590

2591 3
class VersionListProxy(ListProxy):
2592 3
    dataType = VersionProxy
2593

2594

2595
########################################################################################
2596

2597
#  Fonts
2598

2599

2600 3
class BillboardListProxy(ListProxy):
2601 3
    dataType = WebResourceURLDataType
2602

2603

2604 3
class Font(DictBasedObject):
2605
    #   key:                    [data type, required, default value, description]
2606 3
    _structure = {
2607
        "name": [
2608
            MultiLanguageTextProxy,
2609
            True,
2610
            None,
2611
            (
2612
                "Human-readable name of font. This may include any additions that you "
2613
                "find useful to communicate to your users."
2614
            ),
2615
        ],
2616
        "uniqueID": [
2617
            StringDataType,
2618
            True,
2619
            None,
2620
            (
2621
                "A machine-readable string that uniquely identifies this font within "
2622
                "the publisher. It will be used to ask for un/installation of the "
2623
                "font from the server in the `installFonts` and `uninstallFonts` "
2624
                "commands. Also, it will be used for the file name of the font on "
2625
                "disk, together with the version string and the file extension. "
2626
                "Together, they must not be longer than 220 characters and must "
2627
                "not contain the following characters: / ? < > \\ : * | ^"
2628
            ),
2629
        ],
2630
        "postScriptName": [
2631
            StringDataType,
2632
            True,
2633
            None,
2634
            "Complete PostScript name of font",
2635
        ],
2636
        "packageKeywords": [
2637
            FontPackageReferencesListProxy,
2638
            False,
2639
            None,
2640
            "List of references to ::FontPackage:: objects by their keyword",
2641
        ],
2642
        "versions": [
2643
            VersionListProxy,
2644
            False,
2645
            None,
2646
            (
2647
                "List of ::Version:: objects. These are font-specific versions; they "
2648
                "may exist only for this font. You may define additional versions at "
2649
                "the family object under ::Family.versions::, which are then expected "
2650
                "to be available for the entire family. However, either the fonts or "
2651
                "the font family *must* carry version information and the validator "
2652
                "will complain when they don’t.\n\nPlease also read the section on "
2653
                "[versioning](#versioning) above."
2654
            ),
2655
        ],
2656
        "designerKeywords": [
2657
            DesignersReferencesListProxy,
2658
            False,
2659
            None,
2660
            (
2661
                "List of keywords referencing designers. These are defined at "
2662
                "::InstallableFontsResponse.designers::. This attribute overrides the "
2663
                "designer definitions at the family level at ::Family.designers::."
2664
            ),
2665
        ],
2666
        "free": [BooleanDataType, False, None, "Font is freeware. For UI signaling"],
2667
        "billboardURLs": [
2668
            BillboardListProxy,
2669
            False,
2670
            None,
2671
            (
2672
                "List of URLs pointing at images to show for this typeface. "
2673
                "We suggest to use square dimensions and uncompressed SVG "
2674
                "images because they scale to all sizes smoothly, "
2675
                "but ultimately any size or HTML-compatible image type "
2676
                "is possible."
2677
            ),
2678
        ],
2679
        "status": [
2680
            FontStatusDataType,
2681
            True,
2682
            "stable",
2683
            "Font status. For UI signaling. Possible values are: %s" % FONTSTATUSES,
2684
        ],
2685
        "variableFont": [
2686
            BooleanDataType,
2687
            False,
2688
            False,
2689
            "Font is an OpenType Variable Font. For UI signaling",
2690
        ],
2691
        "purpose": [
2692
            FontPurposeDataType,
2693
            True,
2694
            None,
2695
            (
2696
                "Technical purpose of font. This influences how the app handles the "
2697
                "font. For instance, it will only install desktop fonts on the system, "
2698
                "and make other font types available though folders. Possible: %s"
2699
                % (list(FONTPURPOSES.keys()))
2700
            ),
2701
        ],
2702
        "format": [
2703
            FontExtensionDataType,
2704
            False,
2705
            None,
2706
            (
2707
                "Font file format. Required value in case of `desktop` font "
2708
                "(see ::Font.purpose::. Possible: %s" % FILEEXTENSIONS
2709
            ),
2710
        ],
2711
        "protected": [
2712
            BooleanDataType,
2713
            False,
2714
            False,
2715
            (
2716
                "Indication that font is (most likely) commercial and requires "
2717
                "a certain amount of special treatment over a free font: "
2718
                "1) The API Endpoint requires a valid subscriptionID to be used "
2719
                "for authentication. 2) The API Endpoint may limit the downloads "
2720
                "of fonts. "
2721
                "3) Most importantly, "
2722
                "the `uninstallFonts` command needs to be called on the "
2723
                "API Endpoint when the font gets uninstalled."
2724
                "This may also be used for fonts that are free to download, but their "
2725
                "installations want to be monitored or limited anyway. "
2726
            ),
2727
        ],
2728
        "dateFirstPublished": [
2729
            DateDataType,
2730
            False,
2731
            None,
2732
            (
2733
                "Human readable date of the initial release of the font. May also be "
2734
                "defined family-wide at ::Family.dateFirstPublished::."
2735
            ),
2736
        ],
2737
        "usedLicenses": [
2738
            LicenseUsageListProxy,
2739
            True,
2740
            None,
2741
            (
2742
                "List of ::LicenseUsage:: objects. These licenses represent the "
2743
                "different ways in which a user has access to this font. At least one "
2744
                "used license must be defined here, because a user needs to know under "
2745
                "which legal circumstances he/she is using the font. Several used "
2746
                "licenses may be defined for a single font in case a customer owns "
2747
                "several licenses that cover the same font. For instance, a customer "
2748
                "could have purchased a font license standalone, but also as part of "
2749
                "the foundry’s entire catalogue. It’s important to keep these separate "
2750
                "in order to provide the user with separate upgrade links where he/she "
2751
                "needs to choose which of several owned licenses needs to be upgraded. "
2752
                "Therefore, in case of a commercial retail foundry, used licenses "
2753
                "correlate to a user’s purchase history."
2754
            ),
2755
        ],
2756
        "pdfURL": [
2757
            WebResourceURLDataType,
2758
            False,
2759
            None,
2760
            (
2761
                "URL of PDF file with type specimen and/or instructions for this "
2762
                "particular font. (See also: ::Family.pdf::"
2763
            ),
2764
        ],
2765
        "expiry": [
2766
            TimestampDataType,
2767
            False,
2768
            None,
2769
            (
2770
                "Unix timestamp of font’s expiry. The font will be deleted on that "
2771
                "moment. This could be set either upon initial installation of a trial "
2772
                "font, or also before initial installation as a general expiry moment."
2773
            ),
2774
        ],
2775
        "expiryDuration": [
2776
            IntegerDataType,
2777
            False,
2778
            None,
2779
            (
2780
                "Minutes for which the user will be able to use the font after initial "
2781
                "installation. This attribute is used only as a visual hint in the UI "
2782
                "and should be set for trial fonts that expire a certain period after "
2783
                "initial installation, such as 60 minutes. If the font is a trial font "
2784
                "limited to a certain usage period after initial installation, it must "
2785
                "also be marked as ::Font.protected::, with no ::Font.expiry:: "
2786
                "timestamp set at first (because the expiry depends on the moment of "
2787
                "initial installation). On initial font installation by the user, the "
2788
                "publisher’s server needs to record that moment’s time, and from there "
2789
                "onwards serve the subscription with ::Font.expiry:: attribute set in "
2790
                "the future. Because the font is marked as ::Font.protected::, the app "
2791
                "will update the subscription directly after font installation, upon "
2792
                "when it will learn of the newly added ::Font.expiry:: attribute. "
2793
                "Please note that you *have* to set ::Font.expiry:: after initial "
2794
                "installation yourself. The Type.World app will not follow up on its "
2795
                "own on installed fonts just with the ::Font.expiryDuration:: "
2796
                "attribute, which is used only for display."
2797
            ),
2798
        ],
2799
        "features": [
2800
            OpenTypeFeatureListProxy,
2801
            False,
2802
            None,
2803
            (
2804
                "List of supported OpenType features as per "
2805
                "https://docs.microsoft.com/en-us/typography/opentype/spec/featuretags"
2806
            ),
2807
        ],
2808
        "languageSupport": [
2809
            LanguageSupportDataType,
2810
            False,
2811
            None,
2812
            "Dictionary of suppported languages as script/language combinations",
2813
        ],
2814
    }
2815

2816 3
    def __repr__(self):
2817 3
        return "<Font '%s'>" % (
2818
            self.postScriptName or self.name.getText() or "undefined"
2819
        )
2820

2821 3
    def sample(self):
2822 3
        o = self.__class__()
2823 3
        o.name.en = "Bold"
2824 3
        o.name.de = "Fette"
2825 3
        o.uniqueID = "AwesomeFonts-AwesomeFamily-Bold"
2826 3
        o.postScriptName = "AwesomeFamily-Bold"
2827 3
        o.purpose = "desktop"
2828 3
        return o
2829

2830 3
    def filename(self, version):
2831
        """\
2832
        Returns the recommended font file name to be used to store the font on disk.
2833

2834
        It is composed of the font’s uniqueID, its version string and the file
2835
        extension. Together, they must not exceed 220 characters.
2836
        """
2837

2838 3
        if not type(version) in (str, int, float):
2839 3
            raise ValueError("Supplied version must be str or int or float")
2840

2841 3
        if self.format:
2842 3
            return "%s_%s.%s" % (self.uniqueID, version, self.format)
2843
        else:
2844 3
            return "%s_%s" % (self.uniqueID, version)
2845

2846 3
    def hasVersionInformation(self):
2847 3
        return self.versions or self.parent.versions
2848

2849 3
    def customValidation(self):
2850 3
        information, warnings, critical = [], [], []
2851

2852
        # Checking font type/extension
2853 3
        if self.purpose == "desktop" and not self.format:
2854 3
            critical.append(
2855
                "Is a desktop font (see .purpose), but has no .format value."
2856
            )
2857

2858
        # Checking version information
2859 3
        if not self.hasVersionInformation():
2860 3
            critical.append(
2861
                (
2862
                    "Has no version information, and neither has its family %s. "
2863
                    "Either one needs to carry version information." % (self.parent)
2864
                )
2865
            )
2866

2867
        # Checking for designers
2868 3
        for designerKeyword in self.designerKeywords:
2869 3
            if not self.parent.parent.parent.getDesignerByKeyword(designerKeyword):
2870 3
                critical.append(
2871
                    "Has designer '%s', but %s.designers has no matching designer."
2872
                    % (designerKeyword, self.parent.parent.parent)
2873
                )
2874

2875
        # Checking uniqueID for file name contradictions:
2876 3
        forbidden = "/?<>\\:*|^,;"
2877 3
        for char in forbidden:
2878 3
            if self.uniqueID.count(char) > 0:
2879 3
                critical.append(
2880
                    (
2881
                        ".uniqueID must not contain the character '%s' because it will "
2882
                        "be used for the font’s file name on disk." % char
2883
                    )
2884
                )
2885

2886 3
        for version in self.getVersions():
2887 3
            filename = self.filename(version.number)
2888 3
            if len(filename) > 220:
2889 3
                critical.append(
2890
                    "The suggested file name is longer than 220 characters: %s"
2891
                    % filename
2892
                )
2893

2894 3
        return information, warnings, critical
2895

2896 3
    def getBillboardURLs(self):
2897
        """\
2898
        Returns list billboardURLs compiled from ::Font.billboardURLs::
2899
        and ::Family.billboardURLs::, giving the font-level definitions priority
2900
        over family-level definitions.
2901
        """
2902 3
        return self.billboardURLs or self.parent.billboardURLs
2903

2904 3
    def getVersions(self):
2905
        """\
2906
        Returns list of ::Version:: objects.
2907

2908
        This is the final list based on the version information in this font object as
2909
        well as in its parent ::Family:: object. Please read the section about
2910
        [versioning](#versioning) above.
2911
        """
2912

2913 3
        if not self.hasVersionInformation():
2914 3
            raise ValueError(
2915
                (
2916
                    "%s has no version information, and neither has its family %s. "
2917
                    "Either one needs to carry version information."
2918
                    % (self, self.parent)
2919
                )
2920
            )
2921

2922 3
        def compare(a, b):
2923 3
            return semver.VersionInfo.parse(makeSemVer(a.number)).compare(
2924
                makeSemVer(b.number)
2925
            )
2926
            # return semver.compare(makeSemVer(a.number), makeSemVer(b.number))
2927

2928 3
        versions = []
2929 3
        haveVersionNumbers = []
2930 3
        for version in self.versions:
2931 3
            versions.append(version)
2932 3
            haveVersionNumbers.append(makeSemVer(version.number))
2933 3
        for version in self.parent.versions:
2934 3
            if version.number not in haveVersionNumbers:
2935 3
                versions.append(version)
2936 3
                haveVersionNumbers.append(makeSemVer(version.number))
2937

2938 3
        versions = sorted(versions, key=functools.cmp_to_key(compare))
2939

2940 3
        return versions
2941

2942 3
    def getDesigners(self):
2943
        """\
2944
        Returns a list of ::Designer:: objects that this font references.
2945
        These are the combination of family-level designers and font-level designers.
2946
        The same logic as for versioning applies.
2947
        Please read the section about [versioning](#versioning) above.
2948
        """
2949 3
        if not hasattr(self, "_designers"):
2950 3
            self._designers = []
2951

2952
            # Family level designers
2953 3
            if self.parent.designerKeywords:
2954 3
                for designerKeyword in self.parent.designerKeywords:
2955 3
                    self._designers.append(
2956
                        self.parent.parent.parent.getDesignerByKeyword(designerKeyword)
2957
                    )
2958

2959
            # Font level designers
2960 3
            if self.designerKeywords:
2961 3
                for designerKeyword in self.designerKeywords:
2962 3
                    self._designers.append(
2963
                        self.parent.parent.parent.getDesignerByKeyword(designerKeyword)
2964
                    )
2965

2966 3
        return self._designers
2967

2968 3
    def getPackageKeywords(self):
2969 3
        if self.packageKeywords:
2970 3
            return list(set(self.packageKeywords))
2971
        else:
2972 3
            return [DEFAULT]
2973

2974

2975 3
def Font_Parent(self):
2976 3
    if (
2977
        hasattr(self, "_parent")
2978
        and hasattr(self._parent, "_parent")
2979
        and hasattr(self._parent._parent, "_parent")
2980
    ):
2981 3
        return self._parent._parent._parent
2982

2983

2984 3
Font.parent = property(lambda self: Font_Parent(self))
2985

2986

2987 3
class FontProxy(Proxy):
2988 3
    dataType = Font
2989

2990

2991 3
class FontListProxy(ListProxy):
2992 3
    dataType = FontProxy
2993

2994

2995
# Font Family
2996

2997

2998 3
class Family(DictBasedObject):
2999
    #   key:                    [data type, required, default value, description]
3000 3
    _structure = {
3001
        "uniqueID": [
3002
            StringDataType,
3003
            True,
3004
            None,
3005
            "An string that uniquely identifies this family within the publisher.",
3006
        ],
3007
        "name": [
3008
            MultiLanguageTextProxy,
3009
            True,
3010
            None,
3011
            (
3012
                "Human-readable name of font family. This may include any additions "
3013
                "that you find useful to communicate to your users."
3014
            ),
3015
        ],
3016
        "description": [
3017
            MultiLanguageLongTextProxy,
3018
            False,
3019
            None,
3020
            "Description of font family",
3021
        ],
3022
        "billboardURLs": [
3023
            BillboardListProxy,
3024
            False,
3025
            None,
3026
            (
3027
                "List of URLs pointing at images to show for this typeface. "
3028
                "We suggest to use square dimensions and uncompressed SVG "
3029
                "images because they scale to all sizes smoothly, "
3030
                "but ultimately any size or HTML-compatible image type "
3031
                "is possible."
3032
            ),
3033
        ],
3034
        "designerKeywords": [
3035
            DesignersReferencesListProxy,
3036
            False,
3037
            None,
3038
            (
3039
                "List of keywords referencing designers. These are defined at "
3040
                "::InstallableFontsResponse.designers::. In case designers differ "
3041
                "between fonts within the same family, they can also be defined at the "
3042
                "font level at ::Font.designers::. The font-level references take "
3043
                "precedence over the family-level references."
3044
            ),
3045
        ],
3046
        "packages": [
3047
            FontPackageListProxy,
3048
            False,
3049
            None,
3050
            (
3051
                "Family-wide list of ::FontPackage:: objects. These will be "
3052
                "referenced by their keyword in ::Font.packageKeywords::"
3053
            ),
3054
        ],
3055
        "sourceURL": [
3056
            WebURLDataType,
3057
            False,
3058
            None,
3059
            "URL pointing to the source of a font project, such as a GitHub repository",
3060
        ],
3061
        "issueTrackerURL": [
3062
            WebURLDataType,
3063
            False,
3064
            None,
3065
            (
3066
                "URL pointing to an issue tracker system, where users can debate "
3067
                "about a typeface’s design or technicalities"
3068
            ),
3069
        ],
3070
        "galleryURL": [
3071
            WebURLDataType,
3072
            False,
3073
            None,
3074
            (
3075
                "URL pointing to a web site that shows real world examples of the "
3076
                "fonts in use or other types of galleries."
3077
            ),
3078
        ],
3079
        "versions": [
3080
            VersionListProxy,
3081
            False,
3082
            None,
3083
            (
3084
                "List of ::Version:: objects. Versions specified here are expected to "
3085
                "be available for all fonts in the family, which is probably most "
3086
                "common and efficient. You may define additional font-specific "
3087
                "versions at the ::Font:: object. You may also rely entirely on "
3088
                "font-specific versions and leave this field here empty. However, "
3089
                "either the fonts or the font family *must* carry version information "
3090
                "and the validator will complain when they don’t.\n\nPlease also read "
3091
                "the section on [versioning](#versioning) above."
3092
            ),
3093
        ],
3094
        "fonts": [
3095
            FontListProxy,
3096
            True,
3097
            None,
3098
            (
3099
                "List of ::Font:: objects. The order will be displayed unchanged in "
3100
                "the UI, so it’s in your responsibility to order them correctly."
3101
            ),
3102
        ],
3103
        "dateFirstPublished": [
3104
            DateDataType,
3105
            False,
3106
            None,
3107
            (
3108
                "Human readable date of the initial release of the family. May be "
3109
                "overriden on font level at ::Font.dateFirstPublished::."
3110
            ),
3111
        ],
3112
        "pdfURL": [
3113
            WebResourceURLDataType,
3114
            False,
3115
            None,
3116
            (
3117
                "URL of PDF file with type specimen and/or instructions for entire "
3118
                "family. May be overriden on font level at ::Font.pdf::."
3119
            ),
3120
        ],
3121
    }
3122

3123 3
    def sample(self):
3124 3
        o = self.__class__()
3125 3
        o.name.en = "Awesome Family"
3126 3
        o.description.en = "Nice big fat face with smooth corners"
3127 3
        o.description.de = "Fette Groteske mit runden Ecken"
3128 3
        o.uniqueID = "AwesomeFonts-AwesomeFamily"
3129 3
        return o
3130

3131 3
    def __repr__(self):
3132 3
        return "<Family '%s'>" % self.name.getText() or "undefined"
3133

3134 3
    def customValidation(self):
3135 3
        information, warnings, critical = [], [], []
3136

3137
        # Checking for designers
3138 3
        for designerKeyword in self.designerKeywords:
3139 3
            if not self.parent.parent.getDesignerByKeyword(designerKeyword):
3140 3
                critical.append(
3141
                    "Has designer '%s', but %s.designers has no matching designer."
3142
                    % (designerKeyword, self.parent.parent)
3143
                )
3144

3145 3
        return information, warnings, critical
3146

3147 3
    def getDesigners(self):
3148 3
        if not hasattr(self, "_designers"):
3149 3
            self._designers = []
3150 3
            for designerKeyword in self.designerKeywords:
3151 3
                self._designers.append(
3152
                    self.parent.parent.getDesignerByKeyword(designerKeyword)
3153
                )
3154 3
        return self._designers
3155

3156 3
    def getAllDesigners(self):
3157
        """\
3158
        Returns a list of ::Designer:: objects that represent all of the designers
3159
        referenced both at the family level as well as with all the family’s fonts,
3160
        in case the fonts carry specific designers. This could be used to give a
3161
        one-glance overview of all designers involved.
3162
        """
3163 3
        if not hasattr(self, "_allDesigners"):
3164 3
            self._allDesigners = []
3165 3
            self._allDesignersKeywords = []
3166 3
            for designerKeyword in self.designerKeywords:
3167 3
                self._allDesigners.append(
3168
                    self.parent.parent.getDesignerByKeyword(designerKeyword)
3169
                )
3170 3
                self._allDesignersKeywords.append(designerKeyword)
3171 3
            for font in self.fonts:
3172 3
                for designerKeyword in font.designerKeywords:
3173 3
                    if designerKeyword not in self._allDesignersKeywords:
3174 3
                        self._allDesigners.append(
3175
                            self.parent.parent.getDesignerByKeyword(designerKeyword)
3176
                        )
3177 3
                        self._allDesignersKeywords.append(designerKeyword)
3178 3
        return self._allDesigners
3179

3180 3
    def getPackages(self, filterByFontPurpose=[]):
3181

3182 3
        packageKeywords = []
3183 3
        packages = []
3184 3
        packageByKeyword = {}
3185

3186 3
        def passedFilter(font):
3187
            # Apply font.purpose filter
3188 3
            return not filterByFontPurpose or font.purpose in filterByFontPurpose
3189

3190
        # Collect list of unique package keyword references in family's fonts
3191 3
        for font in self.fonts:
3192 3
            if passedFilter(font):
3193 3
                for keyword in font.getPackageKeywords():
3194 3
                    if keyword not in packageKeywords:
3195 3
                        packageKeywords.append(keyword)
3196

3197
        # Prepend a DEFAULT package
3198 3
        if DEFAULT in packageKeywords:
3199 3
            defaultPackage = FontPackage()
3200 3
            defaultPackage.keyword = DEFAULT