From ee3cda0b781e1f3b97c13bdd4215e9d5b5e187e4 Mon Sep 17 00:00:00 2001 From: Nafie Alhelali Date: Wed, 13 Jul 2022 09:23:59 +0300 Subject: [PATCH 1/2] style: lint, format and restructure files --- .github/workflows/on_pull_request.yml | 39 ++ .gitignore | 18 + pyproject.toml | 19 + setup.py | 2 +- wallet/.flake8 | 5 + wallet/Pass.py | 271 +++++++++ wallet/PassInformation/PassInformation.py | 76 +++ wallet/PassInformation/__init__.py | 1 + wallet/PassProps/Alignment.py | 8 + wallet/PassProps/Barcode.py | 34 ++ wallet/PassProps/DateStyle.py | 8 + wallet/PassProps/Field.py | 130 +++++ wallet/PassProps/IBeacon.py | 21 + wallet/PassProps/Location.py | 30 + wallet/PassProps/NumberStyle.py | 7 + wallet/PassProps/TransitType.py | 8 + wallet/PassProps/__init__.py | 8 + wallet/PassStyles/BoardingPass.py | 16 + wallet/PassStyles/Coupon.py | 9 + wallet/PassStyles/EventTicket.py | 9 + wallet/PassStyles/Generic.py | 9 + wallet/PassStyles/StoreCard.py | 9 + wallet/PassStyles/__init__.py | 5 + wallet/exceptions.py | 1 + wallet/models.py | 682 ---------------------- wallet/utils/__init__.py | 0 wallet/utils/helpers.py | 0 27 files changed, 742 insertions(+), 683 deletions(-) create mode 100644 .github/workflows/on_pull_request.yml create mode 100644 pyproject.toml create mode 100644 wallet/.flake8 create mode 100644 wallet/Pass.py create mode 100644 wallet/PassInformation/PassInformation.py create mode 100644 wallet/PassInformation/__init__.py create mode 100644 wallet/PassProps/Alignment.py create mode 100644 wallet/PassProps/Barcode.py create mode 100644 wallet/PassProps/DateStyle.py create mode 100644 wallet/PassProps/Field.py create mode 100644 wallet/PassProps/IBeacon.py create mode 100644 wallet/PassProps/Location.py create mode 100644 wallet/PassProps/NumberStyle.py create mode 100644 wallet/PassProps/TransitType.py create mode 100644 wallet/PassProps/__init__.py create mode 100644 wallet/PassStyles/BoardingPass.py create mode 100644 wallet/PassStyles/Coupon.py create mode 100644 wallet/PassStyles/EventTicket.py create mode 100644 wallet/PassStyles/Generic.py create mode 100644 wallet/PassStyles/StoreCard.py create mode 100644 wallet/PassStyles/__init__.py delete mode 100644 wallet/models.py create mode 100644 wallet/utils/__init__.py create mode 100644 wallet/utils/helpers.py diff --git a/.github/workflows/on_pull_request.yml b/.github/workflows/on_pull_request.yml new file mode 100644 index 0000000..c8751ca --- /dev/null +++ b/.github/workflows/on_pull_request.yml @@ -0,0 +1,39 @@ +name: Checks +on: + pull_request: + branches: + - "develop" + - "master" + workflow_dispatch: +jobs: + commitlint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: wagoid/commitlint-github-action@v2 + codelint: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.10] + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - name: Lint with flake8 + run: | + python -m pip install --upgrade pip + pip install flake8 + flake8 . --ignore=E203,W605,W503 --count --show-source --max-complexity=16 --max-line-length=127 \ + --statistics --exclude .git,__pycache__,__init__.py + unittests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: start the app & run unit tests + shell: bash + run: | + python -m pip install --upgrade pip + pip install pytest + pytest \ No newline at end of file diff --git a/.gitignore b/.gitignore index 11d5e80..3aea80e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ *.py[co] *.py~ +# pytest +.pytest_cache +__pycache__ + # Packages MANIFEST *.egg @@ -31,3 +35,17 @@ pip-log.txt .DS_Store .idea/ *.swp + +venv +.vscode +poetry.lock + +wallet/tmp + +.env + +generate_test_pass.py + +test_pass.pkpass + +icon2x.png \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f0a34b4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "py-pkpass" +version = "0.1.1" +description = "python library to create pkpass files (Apple Wallet files)" +authors = [] +license = "MIT license" + +[tool.poetry.dependencies] +python = "^3.10" + +[tool.poetry.dev-dependencies] +six = "^1.16.0" +black = "^22.6.0" +flake8 = "^4.0.1" +pytest = "^7.1.2" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/setup.py b/setup.py index 07f47cc..4fe655f 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ install_requires=install_requires, - classifiers = [ + classifiers=[ 'Development Status :: 3 - Alpha', 'Environment :: Other Environment', 'Intended Audience :: Developers', diff --git a/wallet/.flake8 b/wallet/.flake8 new file mode 100644 index 0000000..6dbc85d --- /dev/null +++ b/wallet/.flake8 @@ -0,0 +1,5 @@ +[flake8] +max-line-length= 127 +ignore = E203,W605,W503 +max-complexity= 16 +exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache \ No newline at end of file diff --git a/wallet/Pass.py b/wallet/Pass.py new file mode 100644 index 0000000..13b4c45 --- /dev/null +++ b/wallet/Pass.py @@ -0,0 +1,271 @@ +import decimal +import hashlib +from io import BytesIO +import json +import subprocess +import zipfile +import tempfile + +from .exceptions import PassParameterException + + +def pass_handler(obj): + """Pass Handler""" + if hasattr(obj, "json_dict"): + return obj.json_dict() + # For Decimal latitude and logitude etc. + if isinstance(obj, decimal.Decimal): + return str(obj) + return obj + + +class Pass: + def __init__(self, **kwargs): + """ + Prepare Pass + :params pass_information: + :params pass_type_identifier: + :params organization_name: + :params team_identifier: + """ + + self._files = {} # Holds the files to include in the .pkpass + self._hashes = {} # Holds the SHAs of the files array + + # Standard Keys + + # Required. Team identifier of the organization that originated and + # signed the pass, as issued by Apple. + self.teamIdentifier = kwargs["team_identifier"] + # Required. Pass type identifier, as issued by Apple. The value must + # correspond with your signing certificate. Used for grouping. + self.passTypeIdentifier = kwargs["pass_type_identifier"] + # Required. Display name of the organization that originated and + # signed the pass. + self.organizationName = kwargs["organization_name"] + # Required. Serial number that uniquely identifies the pass. + self.serialNumber = "" + # Required. Brief description of the pass, used by the iOS + # accessibility technologies. + self.description = "" + # Required. + self.formatVersion = 1 + + # Visual Appearance Keys + self.backgroundColor = None # Optional. Background color of the pass + self.foregroundColor = None # Optional. Foreground color of the pass, + self.labelColor = None # Optional. Color of the label text + self.logoText = None # Optional. Text displayed next to the logo + self.barcode = None # Optional. Information specific to barcodes. + self.barcodes = [] + + # Optional. If true, the strip image is displayed + self.suppressStripShine = False + + # Web Service Keys + + # Optional. If present, authenticationToken must be supplied + self.webServiceURL = None + # The authentication token to use with the web service + self.authenticationToken = None + + # Relevance Keys + + # Optional. Locations where the pass is relevant. + # For example, the location of your store. + self.locations = [] + # Optional. IBeacons data + self.ibeacons = [] + # Optional. Date and time when the pass becomes relevant + self.relevantDate = None + + # Optional. A list of iTunes Store item identifiers for + # the associated apps. + self.associatedStoreIdentifiers = None + self.appLaunchURL = None + # Optional. Additional hidden data in json for the passbook + self.userInfo = None + + self.exprirationDate = None + self.voided = None + + self.passInformation = kwargs["pass_information"] + + def add_file(self, name, file_handle): + """ + Add file to the file + :params name: String name + :params file_handle: File Handle + """ + self._files[name] = file_handle.read() + + # Creates the actual .pkpass file + def create( + self, + certificate, + key, + wwdr_certificate, + password=False, + file_name=None, + filemode=True, + ): + """ + Create .pkass File + """ + pass_json = self._create_pass_json() + manifest = self._create_manifest(pass_json) + signature = self._create_signature( + manifest, certificate, key, wwdr_certificate, password, filemode + ) + if not file_name: + file_name = BytesIO() + datei = self._create_zip( + pass_json, + manifest, + signature, + file_name=file_name + ) + return datei + + def _create_pass_json(self): + """ + Create Json Pass Files + """ + return json.dumps(self, default=pass_handler).encode("utf-8") + + def _create_manifest(self, pass_json): + """ + Creates the hashes for the files and adds them + into a json string + """ + # Creates SHA hashes for all files in package + self._hashes["pass.json"] = hashlib.sha1(pass_json).hexdigest() + for filename, filedata in self._files.items(): + self._hashes[filename] = hashlib.sha1(filedata).hexdigest() + return json.dumps(self._hashes).encode("utf-8") + + def _create_signature( + self, manifest, certificate, key, wwdr_certificate, password, filemode + ): + """Create and Save Signature""" + if not filemode: + # Use Tempfile + cert_file = tempfile.NamedTemporaryFile(mode="w") + cert_file.write(certificate) + cert_file.flush() + key_file = tempfile.NamedTemporaryFile(mode="w") + key_file.write(key) + key_file.flush() + wwdr_file = tempfile.NamedTemporaryFile(mode="w") + wwdr_file.write(wwdr_certificate) + wwdr_file.flush() + certificate = cert_file.name + key = key_file.name + wwdr_certificate = wwdr_file.name + + openssl_cmd = [ + "openssl", + "smime", + "-binary", + "-sign", + "-certfile", + wwdr_certificate, + "-signer", + certificate, + "-inkey", + key, + "-outform", + "DER", + "-passin", + f"pass:{password}", + ] + + process = subprocess.Popen( + openssl_cmd, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + ) + process.stdin.write(manifest) + der, error = process.communicate() + if process.returncode != 0: + raise Exception(error) + + return der + + # Creates .pkpass (zip archive) + def _create_zip(self, pass_json, manifest, signature, file_name=None): + """ + Creats .pkass ZIP Archive + """ + z_file = zipfile.ZipFile(file_name or "pass.pkpass", "w") + z_file.writestr("signature", signature) + z_file.writestr("manifest.json", manifest) + z_file.writestr("pass.json", pass_json) + for filename, filedata in self._files.items(): + z_file.writestr(filename, filedata) + z_file.close() + return file_name + + def json_dict(self): + """ + Return Pass as JSON Dict + """ + simple_fields = [ + "description", + "formatVersion", + "organizationName", + "passTypeIdentifier", + "serialNumber", + "teamIdentifier", + "suppressStripShine" "relevantDate", + "backgroundColor", + "foregroundColor", + "labelColor", + "logoText", + "ibeacons", + "userInfo", + "voided", + "associatedStoreIdentifiers", + "appLaunchURL", + "exprirationDate", + "webServiceURL", + "authenticationToken", + ] + data = {} + data[self.passInformation.jsonname] = self.passInformation.json_dict() + for field in simple_fields: + if hasattr(self, field): + content = getattr(self, field) + if content: + data[field] = content + + if self.barcodes: + data["barcodes"] = [] + for barcode in self.barcodes: + data["barcodes"].append(barcode.json_dict()) + + if self.locations: + data["locations"] = [] + for location in self.locations: + data["locations"].append(location.json_dict()) + if len(data["locations"]) >= 10: + raise PassParameterException("Field locations has<10 entries") + + if self.ibeacons: + data["ibeacons"] = [] + for ibeacon in self.ibeacons: + data["ibeacons"].append(ibeacon.json_dict()) + + requied_fields = [ + "description", + "formatVersion", + "organizationName", + "organizationName", + "serialNumber", + "teamIdentifier", + ] + for field in requied_fields: + if field not in data: + raise PassParameterException(f"Field {field} missing") + return data diff --git a/wallet/PassInformation/PassInformation.py b/wallet/PassInformation/PassInformation.py new file mode 100644 index 0000000..d722de5 --- /dev/null +++ b/wallet/PassInformation/PassInformation.py @@ -0,0 +1,76 @@ +from wallet.PassProps import Field + + +class PassInformation: + """ + Basic Fields for Wallet Pass + """ + + def __init__(self): + self.headerFields = [] + self.primaryFields = [] + self.secondaryFields = [] + self.backFields = [] + self.auxiliaryFields = [] + + def add_header_field(self, **kwargs): + """ + Add Simple Field to Header + :param key: + :param value: + :param label: optional + """ + self.headerFields.append(Field(**kwargs)) + + def add_primary_field(self, **kwargs): + """ + Add Simple Primary Field + :param key: + :param value: + :param label: optional + """ + self.primaryFields.append(Field(**kwargs)) + + def add_secondary_field(self, **kwargs): + """ + Add Simple Secondary Field + :param key: + :param value: + :param label: optional + """ + self.secondaryFields.append(Field(**kwargs)) + + def add_back_field(self, **kwargs): + """ + Add Simple Back Field + :param key: + :param value: + :param label: optional + """ + self.backFields.append(Field(**kwargs)) + + def add_auxiliary_field(self, **kwargs): + """ + Add Simple Auxilary Field + :param key: + :param value: + :param label: optional + """ + self.auxiliaryFields.append(Field(**kwargs)) + + def json_dict(self): + """ + Create Json object of all Fields + """ + data = {} + for what in [ + "headerFields", + "primaryFields", + "secondaryFields", + "backFields", + "auxiliaryFields", + ]: + if hasattr(self, what): + field_data = [f.json_dict() for f in getattr(self, what)] + data.update({what: field_data}) + return data diff --git a/wallet/PassInformation/__init__.py b/wallet/PassInformation/__init__.py new file mode 100644 index 0000000..59449f2 --- /dev/null +++ b/wallet/PassInformation/__init__.py @@ -0,0 +1 @@ +from .PassInformation import PassInformation \ No newline at end of file diff --git a/wallet/PassProps/Alignment.py b/wallet/PassProps/Alignment.py new file mode 100644 index 0000000..f0fcf33 --- /dev/null +++ b/wallet/PassProps/Alignment.py @@ -0,0 +1,8 @@ +class Alignment: + """Text Alignment""" + + LEFT = "PKTextAlignmentLeft" + CENTER = "PKTextAlignmentCenter" + RIGHT = "PKTextAlignmentRight" + JUSTIFIED = "PKTextAlignmentJustified" + NATURAL = "PKTextAlignmentNatural" diff --git a/wallet/PassProps/Barcode.py b/wallet/PassProps/Barcode.py new file mode 100644 index 0000000..9f29df3 --- /dev/null +++ b/wallet/PassProps/Barcode.py @@ -0,0 +1,34 @@ +class BarcodeFormat: + """Barcode Format""" + + PDF417 = "PKBarcodeFormatPDF417" + QR = "PKBarcodeFormatQR" + AZTEC = "PKBarcodeFormatAztec" + + +class Barcode: + """ + Barcode Field + """ + + def __init__(self, **kwargs): + """ + Initiate Field + + :param message: Message or Payload for Barcdoe + :param format: pdf417/ qr/ aztec + :param encoding: Default utf-8 + :param alt_text: Optional Text displayed near the barcode + """ + self.format = { + "pdf417": BarcodeFormat.PDF417, + "qr": BarcodeFormat.QR, + "aztec": BarcodeFormat.AZTEC, + }.get(kwargs["format"], "qr") + self.message = kwargs["message"] + self.messageEncoding = kwargs.get("encoding", "iso-8859-1") + self.altText = kwargs.get("alt_text", "") + + def json_dict(self): + """Return dict object from class""" + return self.__dict__ diff --git a/wallet/PassProps/DateStyle.py b/wallet/PassProps/DateStyle.py new file mode 100644 index 0000000..1441fd2 --- /dev/null +++ b/wallet/PassProps/DateStyle.py @@ -0,0 +1,8 @@ +class DateStyle: + """Date Style""" + + NONE = "PKDateStyleNone" + SHORT = "PKDateStyleShort" + MEDIUM = "PKDateStyleMedium" + LONG = "PKDateStyleLong" + FULL = "PKDateStyleFull" diff --git a/wallet/PassProps/Field.py b/wallet/PassProps/Field.py new file mode 100644 index 0000000..ae4eca1 --- /dev/null +++ b/wallet/PassProps/Field.py @@ -0,0 +1,130 @@ +from .Alignment import Alignment +from .DateStyle import DateStyle +from .NumberStyle import NumberStyle + + +class Field: + """Wallet Text Field""" + + def __init__(self, **kwargs): + """ + Initiate Field + + :param key: The key must be unique within the scope + :param value: Value of the Field + :param attributed_value: Optional. Attributed value of the field. + :param label: Optional Label Text for field + :param change_message: Optional. update message + :param text_alignment: left/ center/ right, justified, natural + :return: Nothing + + """ + + self.key = kwargs["key"] + if "attributed_value" in kwargs: + self.attributedValue = kwargs["attributed_value"] + self.value = kwargs["value"] + self.label = kwargs.get("label", "") + if "change_message" in kwargs: + self.changeMessage = kwargs[ + "change_message" + ] # Don't Populate key if not needed + self.textAlignment = { + "left": Alignment.LEFT, + "center": Alignment.CENTER, + "right": Alignment.RIGHT, + "justified": Alignment.JUSTIFIED, + "natural": Alignment.NATURAL, + }.get(kwargs.get("text_alignment", "left")) + + def json_dict(self): + """Return dict object from class""" + return self.__dict__ + + +class DateField(Field): + """Wallet Date Field""" + + def __init__(self, **kwargs): + """ + Initiate Field + + :param key: The key must be unique within the scope + :param value: Value of the Field + :param label: Optional Label Text for field + :param change_message: Optional. Supdate message + :param text_alignment: left/ center/ right, justified, natural + :param date_style: none/short/medium/long/full + :param time_style: none/short/medium/long/full + :param is_relativ: True/False + """ + + super(DateField, self).__init__(**kwargs) + styles = { + "none": DateStyle.NONE, + "short": DateStyle.SHORT, + "medium": DateStyle.MEDIUM, + "long": DateStyle.LONG, + "full": DateStyle.FULL, + } + + self.dateStyle = styles.get(kwargs.get("date_style", "short")) + self.timeStyle = styles.get(kwargs.get("time_style", "short")) + self.isRelative = kwargs.get("is_relativ", False) + + def json_dict(self): + """Return dict object from class""" + return self.__dict__ + + +class NumberField(Field): + """Number Field""" + + def __init__(self, **kwargs): + """ + Initiate Field + + :param key: The key must be unique within the scope + :param value: Value of the Field + :param label: Optional Label Text for field + :param change_message: Optional. update message + :param text_alignment: left/ center/ right, justified, natural + :param number_style: decimal/percent/scientific/spellout. + """ + + super(NumberField, self).__init__(**kwargs) + self.numberStyle = { + "decimal": NumberStyle.DECIMAL, + "percent": NumberStyle.PERCENT, + "scientific": NumberStyle.SCIENTIFIC, + "spellout": NumberStyle.SPELLOUT, + }.get(kwargs.get("number_style", "decimal")) + self.value = float(self.value) + + def json_dict(self): + """Return dict object from class""" + return self.__dict__ + + +class CurrencyField(Field): + """Currency Field""" + + def __init__(self, **kwargs): + """ + Initiate Field + + :param key: The key must be unique within the scope + :param value: Value of the Field + :param label: Optional Label Text for field + :param change_message: Optional. update message + :param text_alignment: left/ center/ right, justified, natural + :param currency_code: ISO 4217 currency Code + """ + + super(CurrencyField, self).__init__(**kwargs) + self.currencyCode = kwargs["currency_code"] + self.value = float(self.value) + + def json_dict(self): + """Return dict object from class""" + return self.__dict__ diff --git a/wallet/PassProps/IBeacon.py b/wallet/PassProps/IBeacon.py new file mode 100644 index 0000000..7745af4 --- /dev/null +++ b/wallet/PassProps/IBeacon.py @@ -0,0 +1,21 @@ +class IBeacon: + """iBeacon""" + + def __init__(self, **kwargs): + """ + Create Beacon + :param proximity_uuid: + :param major: + :param minor: + :param relevant_text: Option Text shown when near the ibeacon + """ + + self.proximityUUID = kwargs["proximity_uuid"] + self.major = kwargs["major"] + self.minor = kwargs["minor"] + + self.relevantText = kwargs.get("relevant_text", "") + + def json_dict(self): + """Return dict object from class""" + return self.__dict__ diff --git a/wallet/PassProps/Location.py b/wallet/PassProps/Location.py new file mode 100644 index 0000000..36c3ce5 --- /dev/null +++ b/wallet/PassProps/Location.py @@ -0,0 +1,30 @@ +class Location: + """ + Pass Location Object + """ + + def __init__(self, **kwargs): + """ + Fill Location Object. + + :param latitude: Latitude Float + :param longitude: Longitude Float + :param altitude: optional + :param distance: optional + :param relevant_text: optional + :return: Nothing + + """ + + for name in ["latitude", "longitude", "altitude"]: + try: + setattr(self, name, float(kwargs[name])) + except (ValueError, TypeError, KeyError): + setattr(self, name, 0.0) + if "distance" in kwargs: + self.distance = kwargs["distance"] + self.relevantText = kwargs.get("relevant_text", "") + + def json_dict(self): + """Return dict object from class""" + return self.__dict__ diff --git a/wallet/PassProps/NumberStyle.py b/wallet/PassProps/NumberStyle.py new file mode 100644 index 0000000..3a505b1 --- /dev/null +++ b/wallet/PassProps/NumberStyle.py @@ -0,0 +1,7 @@ +class NumberStyle: + """Number Style""" + + DECIMAL = "PKNumberStyleDecimal" + PERCENT = "PKNumberStylePercent" + SCIENTIFIC = "PKNumberStyleScientific" + SPELLOUT = "PKNumberStyleSpellOut" diff --git a/wallet/PassProps/TransitType.py b/wallet/PassProps/TransitType.py new file mode 100644 index 0000000..9e18f03 --- /dev/null +++ b/wallet/PassProps/TransitType.py @@ -0,0 +1,8 @@ +class TransitType: + """Transit Type for Boarding Passes""" + + AIR = "PKTransitTypeAir" + TRAIN = "PKTransitTypeTrain" + BUS = "PKTransitTypeBus" + BOAT = "PKTransitTypeBoat" + GENERIC = "PKTransitTypeGeneric" diff --git a/wallet/PassProps/__init__.py b/wallet/PassProps/__init__.py new file mode 100644 index 0000000..a345a28 --- /dev/null +++ b/wallet/PassProps/__init__.py @@ -0,0 +1,8 @@ +from .Alignment import Alignment +from .Barcode import Barcode +from .DateStyle import DateStyle +from .Field import Field +from .IBeacon import IBeacon +from .Location import Location +from .NumberStyle import NumberStyle +from .TransitType import TransitType diff --git a/wallet/PassStyles/BoardingPass.py b/wallet/PassStyles/BoardingPass.py new file mode 100644 index 0000000..14c1f97 --- /dev/null +++ b/wallet/PassStyles/BoardingPass.py @@ -0,0 +1,16 @@ +from wallet.PassProps import TransitType +from wallet.PassInformation import PassInformation + + +class BoardingPass(PassInformation): + """Wallet Boarding Pass""" + + def __init__(self, transitType=TransitType.AIR): + super(BoardingPass, self).__init__() + self.transitType = transitType + self.jsonname = "boardingPass" + + def json_dict(self): + data = super(BoardingPass, self).json_dict() + data.update({"transitType": self.transitType}) + return data diff --git a/wallet/PassStyles/Coupon.py b/wallet/PassStyles/Coupon.py new file mode 100644 index 0000000..795d1b0 --- /dev/null +++ b/wallet/PassStyles/Coupon.py @@ -0,0 +1,9 @@ +from wallet.PassInformation import PassInformation + + +class Coupon(PassInformation): + """Wallet Coupon Pass""" + + def __init__(self): + super(Coupon, self).__init__() + self.jsonname = "coupon" diff --git a/wallet/PassStyles/EventTicket.py b/wallet/PassStyles/EventTicket.py new file mode 100644 index 0000000..94aa812 --- /dev/null +++ b/wallet/PassStyles/EventTicket.py @@ -0,0 +1,9 @@ +from wallet.PassInformation import PassInformation + + +class EventTicket(PassInformation): + """Wallet Event Ticket""" + + def __init__(self): + super(EventTicket, self).__init__() + self.jsonname = "eventTicket" diff --git a/wallet/PassStyles/Generic.py b/wallet/PassStyles/Generic.py new file mode 100644 index 0000000..98cff32 --- /dev/null +++ b/wallet/PassStyles/Generic.py @@ -0,0 +1,9 @@ +from wallet.PassInformation import PassInformation + + +class Generic(PassInformation): + """Wallet Generic Pass""" + + def __init__(self): + super(Generic, self).__init__() + self.jsonname = "generic" diff --git a/wallet/PassStyles/StoreCard.py b/wallet/PassStyles/StoreCard.py new file mode 100644 index 0000000..7c6a9c0 --- /dev/null +++ b/wallet/PassStyles/StoreCard.py @@ -0,0 +1,9 @@ +from wallet.PassInformation import PassInformation + + +class StoreCard(PassInformation): + """Wallet Store Card""" + + def __init__(self): + super(StoreCard, self).__init__() + self.jsonname = "storeCard" diff --git a/wallet/PassStyles/__init__.py b/wallet/PassStyles/__init__.py new file mode 100644 index 0000000..b4b00e4 --- /dev/null +++ b/wallet/PassStyles/__init__.py @@ -0,0 +1,5 @@ +from .BoardingPass import BoardingPass +from .Coupon import Coupon +from .EventTicket import EventTicket +from .Generic import Generic +from .StoreCard import StoreCard diff --git a/wallet/exceptions.py b/wallet/exceptions.py index d7363ae..71ee2bd 100644 --- a/wallet/exceptions.py +++ b/wallet/exceptions.py @@ -2,6 +2,7 @@ Wallet Exceptions """ + class PassParameterException(Exception): """ Parameter based Exception diff --git a/wallet/models.py b/wallet/models.py deleted file mode 100644 index 1390026..0000000 --- a/wallet/models.py +++ /dev/null @@ -1,682 +0,0 @@ -""" Apple Passbook """ -# pylint: disable=too-few-public-methods, too-many-instance-attributes -import decimal -import hashlib -from io import BytesIO -import json -import subprocess -import zipfile -import re -import tempfile - -from .exceptions import PassParameterException - - -def check_subfields(fields): - """ Check the fields insised a field list """ - for field in fields: - date = field.get('dateStyle') - time = field.get('timeStyle') - regex \ - = r'^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$' - # need none here because of db model default - if date or time: - match_iso8601 = re.compile(regex).match - if match_iso8601(field['value']) is None: - raise PassParameterException("Date does not match iso8601 format") - -def field_checks(field_name, field_data): - """ Check Field Contents if the valid """ - if field_name == 'webServiceURL': - if not field_data.startswith('https://'): - raise PassParameterException("Webservie Url need to start with https://") - - if field_name == 'serialNumber': - if not isinstance(field_data, str): - raise PassParameterException("Serial Needs to be a String") - if len(field_data) <= 16: - raise PassParameterException("Serial Number to Short") - - if field_name == 'headerFields': - if len(field_data) > 3: - raise PassParameterException("To many Header Fields (>3)") - check_subfields(field_data) - - if field_name == 'primaryFields': - check_subfields(field_data) - - if field_name == 'secondaryFields': - check_subfields(field_data) - - if field_name == 'auxiliaryFields': - check_subfields(field_data) - - if field_name == 'backFields': - check_subfields(field_data) - -class Alignment(): - """ Text Alignment """ - LEFT = 'PKTextAlignmentLeft' - CENTER = 'PKTextAlignmentCenter' - RIGHT = 'PKTextAlignmentRight' - JUSTIFIED = 'PKTextAlignmentJustified' - NATURAL = 'PKTextAlignmentNatural' - - -class BarcodeFormat(): - """ Barcode Format""" - PDF417 = 'PKBarcodeFormatPDF417' - QR = 'PKBarcodeFormatQR' - AZTEC = 'PKBarcodeFormatAztec' - - -class TransitType(): - """ Transit Type for Boarding Passes """ - AIR = 'PKTransitTypeAir' - TRAIN = 'PKTransitTypeTrain' - BUS = 'PKTransitTypeBus' - BOAT = 'PKTransitTypeBoat' - GENERIC = 'PKTransitTypeGeneric' - - -class DateStyle(): - """ Date Style """ - NONE = 'PKDateStyleNone' - SHORT = 'PKDateStyleShort' - MEDIUM = 'PKDateStyleMedium' - LONG = 'PKDateStyleLong' - FULL = 'PKDateStyleFull' - - -class NumberStyle(): - """ Number Style """ - DECIMAL = 'PKNumberStyleDecimal' - PERCENT = 'PKNumberStylePercent' - SCIENTIFIC = 'PKNumberStyleScientific' - SPELLOUT = 'PKNumberStyleSpellOut' - -class Field(): - """ Wallet Text Field""" - - def __init__(self, **kwargs): - """ - Initiate Field - - :param key: The key must be unique within the scope - :param value: Value of the Field - :param attributed_value: Optional. Attributed value of the field. - :param label: Optional Label Text for field - :param change_message: Optional. String that is displayed when the pass is updated - :param text_alignment: left/ center/ right, justified, natural - :return: Nothing - - """ - # pylint: disable=invalid-name - self.key = kwargs['key'] - if 'attributed_value' in kwargs: - self.attributedValue = kwargs['attributed_value'] - self.value = kwargs['value'] - self.label = kwargs.get('label', '') - if 'change_message' in kwargs: - self.changeMessage = kwargs['change_message'] # Don't Populate key if not needed - self.textAlignment = { - 'left': Alignment.LEFT, - 'center': Alignment.CENTER, - 'right': Alignment.RIGHT, - 'justified': Alignment.JUSTIFIED, - 'natural': Alignment.NATURAL, - }.get(kwargs.get('text_alignment', 'left')) - - def json_dict(self): - """ Return dict object from class """ - return self.__dict__ - - -class DateField(Field): - """ Wallet Date Field """ - - def __init__(self, **kwargs): - """ - Initiate Field - - :param key: The key must be unique within the scope - :param value: Value of the Field - :param label: Optional Label Text for field - :param change_message: Optional. String that is displayed when the pass is updated - :param text_alignment: left/ center/ right, justified, natural - :param date_style: none/short/medium/long/full - :param time_style: none/short/medium/long/full - :param is_relativ: True/False - """ - #pylint: disable=invalid-name - - super(DateField, self).__init__(**kwargs) - styles = { - "none": DateStyle.NONE, - "short": DateStyle.SHORT, - "medium": DateStyle.MEDIUM, - "long": DateStyle.LONG, - "full": DateStyle.FULL, - } - - self.dateStyle = styles.get(kwargs.get('date_style', 'short')) - self.timeStyle = styles.get(kwargs.get('time_style', 'short')) - self.isRelative = kwargs.get('is_relativ', False) - - def json_dict(self): - """ Return dict object from class """ - return self.__dict__ - - -class NumberField(Field): - """ Number Field """ - - def __init__(self, **kwargs): - """ - Initiate Field - - :param key: The key must be unique within the scope - :param value: Value of the Field - :param label: Optional Label Text for field - :param change_message: Optional. String that is displayed when the pass is updated - :param text_alignment: left/ center/ right, justified, natural - :param number_style: decimal/percent/scientific/spellout. - """ - #pylint: disable=invalid-name - - super(NumberField, self).__init__(**kwargs) - self.numberStyle = { - 'decimal' : NumberStyle.DECIMAL, - 'percent' : NumberStyle.PERCENT, - 'scientific' : NumberStyle.SCIENTIFIC, - 'spellout' : NumberStyle.SPELLOUT, - }.get(kwargs.get('number_style', 'decimal')) - self.value = float(self.value) - - def json_dict(self): - """ Return dict object from class """ - return self.__dict__ - - -class CurrencyField(Field): - """ Currency Field """ - - def __init__(self, **kwargs): - """ - Initiate Field - - :param key: The key must be unique within the scope - :param value: Value of the Field - :param label: Optional Label Text for field - :param change_message: Optional. String that is displayed when the pass is updated - :param text_alignment: left/ center/ right, justified, natural - :param currency_code: ISO 4217 currency Code - """ - #pylint: disable=invalid-name - - super(CurrencyField, self).__init__(**kwargs) - self.currencyCode = kwargs['currency_code'] - self.value = float(self.value) - - def json_dict(self): - """ Return dict object from class """ - return self.__dict__ - - -class Barcode(): - """ - Barcode Field - """ - - def __init__(self, **kwargs): - """ - Initiate Field - - :param message: Message or Payload for Barcdoe - :param format: pdf417/ qr/ aztec - :param encoding: Default utf-8 - :param alt_text: Optional Text displayed near the barcode - """ - - #pylint: disable=invalid-name - self.format = { - 'pdf417' : BarcodeFormat.PDF417, - 'qr' : BarcodeFormat.QR, - 'aztec' : BarcodeFormat.AZTEC, - }.get(kwargs['format'], 'qr') - self.message = kwargs['message'] - self.messageEncoding = kwargs.get('encoding', 'iso-8859-1') - self.altText = kwargs.get('alt_text', '') - - def json_dict(self): - """ Return dict object from class """ - return self.__dict__ - - -class Location(): - """ - Pass Location Object - """ - - def __init__(self, **kwargs): - """ - Fill Location Object. - - :param latitude: Latitude Float - :param longitude: Longitude Float - :param altitude: optional - :param distance: optional - :param relevant_text: optional - :return: Nothing - - """ - # pylint: disable=invalid-name - - for name in ['latitude', 'longitude', 'altitude']: - try: - setattr(self, name, float(kwargs[name])) - except (ValueError, TypeError, KeyError): - setattr(self, name, 0.0) - if 'distance' in kwargs: - self.distance = kwarg['distance'] - self.relevantText = kwargs.get('relevant_text', '') - - def json_dict(self): - """ Return dict object from class """ - return self.__dict__ - - -class IBeacon(): - """ iBeacon """ - - def __init__(self, **kwargs): - """ - Create Beacon - :param proximity_uuid: - :param major: - :param minor: - :param relevant_text: Option Text shown when near the ibeacon - """ - # pylint: disable=invalid-name - - self.proximityUUID = kwargs['proximity_uuid'] - self.major = kwargs['major'] - self.minor = kwargs['minor'] - - self.relevantText = kwargs.get('relevant_text', '') - - def json_dict(self): - """ Return dict object from class """ - return self.__dict__ - - -class PassInformation(): - """ - Basis Fields for Wallet Passes - """ - - def __init__(self): - # pylint: disable=invalid-name - self.headerFields = [] - self.primaryFields = [] - self.secondaryFields = [] - self.backFields = [] - self.auxiliaryFields = [] - - def add_header_field(self, **kwargs): - """ - Add Simple Field to Header - :param key: - :param value: - :param label: optional - """ - self.headerFields.append(Field(**kwargs)) - - def add_primary_field(self, **kwargs): - """ - Add Simple Primary Field - :param key: - :param value: - :param label: optional - """ - self.primaryFields.append(Field(**kwargs)) - - def add_secondary_field(self, **kwargs): - """ - Add Simple Secondary Field - :param key: - :param value: - :param label: optional - """ - self.secondaryFields.append(Field(**kwargs)) - - def add_back_field(self, **kwargs): - """ - Add Simple Back Field - :param key: - :param value: - :param label: optional - """ - self.backFields.append(Field(**kwargs)) - - def add_auxiliary_field(self, **kwargs): - """ - Add Simple Auxilary Field - :param key: - :param value: - :param label: optional - """ - self.auxiliaryFields.append(Field(**kwargs)) - - def json_dict(self): - """ - Create Json object of all Fields - """ - data = {} - for what in ['headerFields', 'primaryFields', 'secondaryFields', - 'backFields', 'auxiliaryFields']: - if hasattr(self, what): - field_data = [f.json_dict() for f in getattr(self, what)] - field_checks(what, field_data) - data.update({what: field_data}) - return data - - -class BoardingPass(PassInformation): - """ Wallet Boarding Pass """ - - def __init__(self, transitType=TransitType.AIR): - #pylint: disable=invalid-name - super(BoardingPass, self).__init__() - self.transitType = transitType - self.jsonname = 'boardingPass' - - def json_dict(self): - data = super(BoardingPass, self).json_dict() - data.update({'transitType': self.transitType}) - return data - - -class Coupon(PassInformation): - """ Wallet Coupon Pass """ - - def __init__(self): - super(Coupon, self).__init__() - self.jsonname = 'coupon' - - -class EventTicket(PassInformation): - """ Wallet Event Ticket """ - - def __init__(self): - super(EventTicket, self).__init__() - self.jsonname = 'eventTicket' - - -class Generic(PassInformation): - """ Wallet Generic Pass """ - - def __init__(self): - super(Generic, self).__init__() - self.jsonname = 'generic' - - -class StoreCard(PassInformation): - """ Wallet Store Card """ - - def __init__(self): - super(StoreCard, self).__init__() - self.jsonname = 'storeCard' - - -class Pass(): - """ Apple Wallet Pass Object """ - - def __init__(self, **kwargs): - """ - Prepare Pass - :params pass_information: - :params pass_type_identifier: - :params organization_name: - :params team_identifier: - """ - - #pylint: disable=invalid-name - - self._files = {} # Holds the files to include in the .pkpass - self._hashes = {} # Holds the SHAs of the files array - - # Standard Keys - - # Required. Team identifier of the organization that originated and - # signed the pass, as issued by Apple. - self.teamIdentifier = kwargs['team_identifier'] - # Required. Pass type identifier, as issued by Apple. The value must - # correspond with your signing certificate. Used for grouping. - self.passTypeIdentifier = kwargs['pass_type_identifier'] - # Required. Display name of the organization that originated and - # signed the pass. - self.organizationName = kwargs['organization_name'] - # Required. Serial number that uniquely identifies the pass. - self.serialNumber = '' - # Required. Brief description of the pass, used by the iOS - # accessibility technologies. - self.description = '' - # Required. - self.formatVersion = 1 - - # Visual Appearance Keys - self.backgroundColor = None # Optional. Background color of the pass - self.foregroundColor = None # Optional. Foreground color of the pass, - self.labelColor = None # Optional. Color of the label text - self.logoText = None # Optional. Text displayed next to the logo - self.barcode = None # Optional. Information specific to barcodes. - self.barcodes = [] - - # Optional. If true, the strip image is displayed - self.suppressStripShine = False - - # Web Service Keys - - # Optional. If present, authenticationToken must be supplied - self.webServiceURL = None - # The authentication token to use with the web service - self.authenticationToken = None - - # Relevance Keys - - # Optional. Locations where the pass is relevant. - # For example, the location of your store. - self.locations = [] - # Optional. IBeacons data - self.ibeacons = [] - # Optional. Date and time when the pass becomes relevant - self.relevantDate = None - - # Optional. A list of iTunes Store item identifiers for - # the associated apps. - self.associatedStoreIdentifiers = None - self.appLaunchURL = None - # Optional. Additional hidden data in json for the passbook - self.userInfo = None - - self.exprirationDate = None - self.voided = None - - self.passInformation = kwargs['pass_information'] - - def add_file(self, name, file_handle): - """ - Add file to the file - :params name: String name - :params file_handle: File Handle - """ - self._files[name] = file_handle.read() - - # Creates the actual .pkpass file - def create(self, certificate, key, wwdr_certificate, password=False, file_name=None, filemode=True): - """ - Create .pkass File - """ - # pylint: disable=too-many-arguments - pass_json = self._create_pass_json() - manifest = self._create_manifest(pass_json) - signature = self._create_signature(manifest, certificate, key, wwdr_certificate, password, filemode) - if not file_name: - file_name = BytesIO() - datei = self._create_zip(pass_json, manifest, signature, file_name=file_name) - return datei - - def _create_pass_json(self): - """ - Create Json Pass Files - """ - return json.dumps(self, default=pass_handler).encode('utf-8') - - def _create_manifest(self, pass_json): - """ - Creates the hashes for the files and adds them - into a json string - """ - # Creates SHA hashes for all files in package - self._hashes['pass.json'] = hashlib.sha1(pass_json).hexdigest() - for filename, filedata in self._files.items(): - self._hashes[filename] = hashlib.sha1(filedata).hexdigest() - return json.dumps(self._hashes).encode('utf-8') - - def _create_signature(self, manifest, certificate, key, - wwdr_certificate, password, filemode): - """ Create and Save Signature """ - # pylint: disable=no-self-use, too-many-arguments - if not filemode: - #Use Tempfile - cert_file = tempfile.NamedTemporaryFile(mode='w') - cert_file.write(certificate) - cert_file.flush() - key_file = tempfile.NamedTemporaryFile(mode='w') - key_file.write(key) - key_file.flush() - wwdr_file = tempfile.NamedTemporaryFile(mode='w') - wwdr_file.write(wwdr_certificate) - wwdr_file.flush() - certificate = cert_file.name - key = key_file.name - wwdr_certificate = wwdr_file.name - - openssl_cmd = [ - 'openssl', - 'smime', - '-binary', - '-sign', - '-certfile', - wwdr_certificate, - '-signer', - certificate, - '-inkey', - key, - '-outform', - 'DER', - '-passin', - 'pass:{}'.format(password), - ] - - process = subprocess.Popen( - openssl_cmd, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - stdin=subprocess.PIPE, - ) - process.stdin.write(manifest) - der, error = process.communicate() - if process.returncode != 0: - raise Exception(error) - - return der - - - # Creates .pkpass (zip archive) - def _create_zip(self, pass_json, manifest, signature, file_name=None): - """ - Creats .pkass ZIP Archive - """ - z_file = zipfile.ZipFile(file_name or 'pass.pkpass', 'w') - z_file.writestr('signature', signature) - z_file.writestr('manifest.json', manifest) - z_file.writestr('pass.json', pass_json) - for filename, filedata in self._files.items(): - z_file.writestr(filename, filedata) - z_file.close() - return file_name - - - def json_dict(self): - """ - Return Pass as JSON Dict - """ - simple_fields = [ - 'description', - 'formatVersion', - 'organizationName', - 'passTypeIdentifier', - 'serialNumber', - 'teamIdentifier', - 'suppressStripShine' - 'relevantDate', - 'backgroundColor', - 'foregroundColor', - 'labelColor', - 'logoText', - 'ibeacons', - 'userInfo', - 'voided', - 'associatedStoreIdentifiers', - 'appLaunchURL', - 'exprirationDate', - 'webServiceURL', - 'authenticationToken', - ] - data = {} - data[self.passInformation.jsonname] = self.passInformation.json_dict() - for field in simple_fields: - if hasattr(self, field): - content = getattr(self, field) - if content: - field_checks(field, content) - data[field] = content - - if self.barcodes: - data['barcodes'] = [] - for barcode in self.barcodes: - data['barcodes'].append(barcode.json_dict()) - - if self.locations: - data['locations'] = [] - for location in self.locations: - data['locations'].append(location.json_dict()) - if len(data['locations']) >= 10: - raise PassParameterException("Field locations has more then 10 entries") - - if self.ibeacons: - data['ibeacons'] = [] - for ibeacon in self.ibeacons: - data['ibeacons'].append(ibeacon.json_dict()) - - requied_fields = [ - 'description', 'formatVersion', - 'organizationName', 'organizationName', - 'serialNumber', 'teamIdentifier' - ] - for field in requied_fields: - if field not in data: - raise PassParameterException("Field {} missing".format(field)) - return data - - -def pass_handler(obj): - """ Pass Handler """ - if hasattr(obj, 'json_dict'): - return obj.json_dict() - # For Decimal latitude and logitude etc. - if isinstance(obj, decimal.Decimal): - return str(obj) - return obj diff --git a/wallet/utils/__init__.py b/wallet/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wallet/utils/helpers.py b/wallet/utils/helpers.py new file mode 100644 index 0000000..e69de29 From 4bc10f13d270e0a2c02b59e78c005e8abeed62e0 Mon Sep 17 00:00:00 2001 From: Nafie Alhelali Date: Wed, 13 Jul 2022 09:34:12 +0300 Subject: [PATCH 2/2] test: dummy test --- wallet/test/test_wallet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wallet/test/test_wallet.py b/wallet/test/test_wallet.py index e69de29..0c4bd9c 100644 --- a/wallet/test/test_wallet.py +++ b/wallet/test/test_wallet.py @@ -0,0 +1,2 @@ +def test_dummy(): + assert 1 == 1