├── .github └── workflows │ └── python-package.yml ├── .gitignore ├── .pylintrc ├── .vscode └── settings.json ├── README.md ├── flyyer ├── __init__.py └── flyyer.py ├── poetry.lock ├── pyproject.toml └── tests ├── __init__.py └── test_flyyer.py /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | python-version: [3.6, 3.7, 3.8, 3.9] 17 | poetry-version: [1.0, 1.1.2] 18 | os: [ubuntu-18.04, macos-latest, windows-latest] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Run image 26 | uses: abatilo/actions-poetry@v2.0.0 27 | with: 28 | poetry-version: ${{ matrix.poetry-version }} 29 | - run: poetry install --no-interaction 30 | # - run: poetry shell --no-interaction 31 | - run: poetry run pytest 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-related 2 | venv 3 | .venv 4 | *.egg 5 | *.egg-info 6 | __pycache__ 7 | dist/ 8 | 9 | # Mac OS 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | 3 | disable=missing-class-docstring,missing-function-docstring 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": true, 3 | "python.linting.enabled": true, 4 | "python.formatting.provider": "black" 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flyyer-python 2 | 3 | ![PyPI - Version](https://img.shields.io/pypi/v/flyyer) ![PyPI - Downloads](https://img.shields.io/pypi/dm/flyyer) 4 | 5 | The AI-powered preview system built from your website (no effort required). 6 | 7 | ![Flyyer live image](https://github.com/useflyyer/create-flyyer-app/blob/master/.github/assets/website-to-preview.png?raw=true&v=1) 8 | 9 | **This module is agnostic to any Python framework.** 10 | 11 | ## Index 12 | 13 | - [Get started (5 minutes)](#get-started-5-minutes) 14 | - [Advanced usage](#advanced-usage) 15 | - [Flyyer Render](#flyyer-render) 16 | - [Development](#development) 17 | - [Test](#test) 18 | 19 | ## Get started (5 minutes) 20 | 21 | Haven't registered your website yet? Go to [Flyyer.io](https://flyyer.io?ref=flyyer-python) and create a project (e.g. `website-com`). 22 | 23 | ### 1. Install the library 24 | 25 | This module requires Python >= 3.6. 26 | 27 | Install it with [Poetry](https://python-poetry.org/). 28 | 29 | ```sh 30 | poetry add flyyer 31 | ``` 32 | 33 | Or install it with [pip](https://pip.pypa.io/en/stable/). 34 | 35 | ```sh 36 | pip install flyyer 37 | ``` 38 | 39 | ### 2. Get your Flyyer.io smart image link 40 | 41 | In your website code (e.g. your landing or product/post view file), set the following: 42 | 43 | ```python 44 | from flyyer import Flyyer 45 | 46 | flyyer = Flyyer( 47 | # Your project slug 48 | project="website-com", 49 | # The current path of your website 50 | path="/path/to/product", # In Django you can use {{ request.get_full_path }} 51 | # (Optional, Recommended) Default or main image for each page 52 | default="/static/image-1.png" # or https://your-site.com/static/image-1.png 53 | ) 54 | 55 | # Check: 56 | print(flyyer.href()) 57 | # > https://cdn.flyyer.io/v2/website-com/_/__v=1618281823&_def=%2Fstatic%2Fimage-1.png/path/to/product 58 | ``` 59 | 60 | ### 3. Put your smart image link in your `` tags 61 | 62 | You'll get the best results like this: 63 | 64 | ```python 65 | 66 | 67 | 68 | ``` 69 | 70 | ### 4. Create a `rule` for your project 71 | 72 | Go to your dashboard [here](https://flyyer.io/dashboard/_/projects/_/manage) and create a rule like the following: 73 | 74 | [![Flyyer basic rule example](https://github.com/useflyyer/create-flyyer-app/blob/master/.github/assets/rule-example.png?raw=true&v=1)](https://flyyer.io/dashboard) 75 | 76 | Voilà! 77 | 78 | ## Advanced usage 79 | 80 | Advanced features include: 81 | 82 | - Custom variables: additional information for your preview that is not present in your website. [Note: if you need customization you should take a look at [Flyyer Render](#flyyer-render)] 83 | - Custom metadata: set custom width, height, resolution, and more (see example). 84 | - Signed URLs. 85 | 86 | Here you have a detailed full example for project `website-com` and path `/path/to/product`. 87 | 88 | ```python 89 | from flyyer import Flyyer, FlyyerMeta 90 | 91 | flyyer = Flyyer( 92 | # [Required] Your project slug, find it in your dashboard https://www.flyyer.io/dashboard/_/projects/_/integrate. 93 | project="website-com", 94 | # [Recommended] The current path of your website (by default it's `/`). 95 | path="/path/to/product", 96 | # [Optional] In case you want to provide information that is not present in your page set it here. 97 | variables={ 98 | "title": "Product name", 99 | "img": "https://flyyer.io/img/marketplace/flyyer-banner.png", 100 | }, 101 | # [Optional] Custom metadata for rendering the image. ID is recommended so we provide you with better statistics. 102 | meta=FlyyerMeta( 103 | id="jeans-123", # recommended for better stats 104 | v="12369420123", # specific handler version, by default it's a random number to circumvent platforms' cache, 105 | width=1200, 106 | height=600, 107 | resolution=0.9, # from 0.0 to 1.0 108 | agent="whatsapp", # force dimensions for specific platform 109 | ), 110 | ) 111 | 112 | # Check: 113 | print(flyyer.href()) 114 | # > https://cdn.flyyer.io/v2/website-com/_/__v=1618281823/path/to/product 115 | ``` 116 | 117 | For signed URLs, just provide your secret (find it in Dashboard > Project > Advanced settings) and choose a strategy (`HMAC` or `JWT`). 118 | 119 | ```python 120 | flyyer = Flyyer( 121 | project="website-com", 122 | path="/path/to/product", 123 | secret="your-secret-key", 124 | strategy="JWT", # or 'HMAC' 125 | ) 126 | 127 | print(flyyer.href()) 128 | # > https://cdn.flyyer.io/v2/website-com/jwt-eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXJhbXMiOnsiX19pZCI6ImplYW5zLTEyMyJ9LCJwYXRoIjoiXC9wYXRoXC90b1wvcHJvZHVjdCJ9.X8Vs5SGEA1-3M6bH-h24jhQnbwH95V_G0f-gPhTBTzE?__v=1618283086 129 | ``` 130 | 131 | ## Flyyer Render 132 | 133 | As you probably realized, **Flyyer** uses the [rules defined on your dashboard](https://flyyer.io/dashboard/_/projects) to decide how to handle every image based on path patterns. It analyses your website to obtain information and then render a content-rich image with no effort. Let's say _"Flyyer delivers images based on the content of this route"_. 134 | 135 | **Flyyer Render** instead requires you to explicitly declare template and variables for the images to render, **giving you more control for customization**. Let's say _"FlyyerRender delivers an image using this template and these explicit variables"_. 136 | 137 | ```python 138 | from flyyer import FlyyerRender 139 | 140 | flyyer = FlyyerRender( 141 | tenant="tenant", 142 | deck="deck", 143 | template="template", 144 | variables={"title": "Hello world!"}, 145 | ) 146 | 147 | # Use this image in your tags 148 | url = flyyer.href() 149 | # > https://cdn.flyyer.io/render/v2/tenant/deck/template.jpeg?__v=1596906866&title=Hello+world%21 150 | ``` 151 | 152 | Variables can be complex arrays and hashes. 153 | 154 | ```python 155 | from flyyer import FlyyerRender, FlyyerMeta 156 | 157 | flyyer = FlyyerRender( 158 | tenant="tenant", 159 | deck="deck", 160 | template="template", 161 | variables={ 162 | "items": [ 163 | { "text": "Oranges", "count": 12 }, 164 | { "text": "Apples", "count": 14 }, 165 | ], 166 | }, 167 | meta=FlyyerMeta( 168 | id="slug-or-id", # To identify the resource in our analytics report 169 | ), 170 | ) 171 | ``` 172 | 173 | You can use signatures with Flyyer Render like below. 174 | 175 | ```python 176 | from flyyer import FlyyerRender 177 | 178 | flyyer = FlyyerRender( 179 | tenant="tenant", 180 | deck="deck", 181 | template="template", 182 | variables={"title": "Hello world!"}, 183 | secret=key, 184 | strategy="HMAC", # JWT 185 | ) 186 | 187 | # Use this image in your tags 188 | url = flyyer.href() 189 | # > https://cdn.flyyer.io/render/v2/tenant/deck/template.jpeg?__v=d+&title=Hello+world%21&__hmac=1bea6d523496848c 190 | ``` 191 | 192 | **IMPORTANT: variables must be serializable.** 193 | 194 | To decode the URL for debugging purposes: 195 | 196 | ```python 197 | from urllib.parse import unquote 198 | 199 | print(unquote(url)) 200 | # > https://cdn.flyyer.io/render/v2/tenant/deck/template.jpeg?title=Hello+world!&__v=123 201 | ``` 202 | 203 | ## Development 204 | 205 | Prepare the local environment: 206 | 207 | ```sh 208 | poetry install 209 | ``` 210 | 211 | ```sh 212 | poetry shell 213 | ``` 214 | 215 | Deploy with: 216 | 217 | ```sh 218 | # Set API Token 219 | poetry config pypi-token.pypi pypi-TOKEN 220 | 221 | poetry version X.Y.Z 222 | poetry build 223 | poetry publish 224 | ``` 225 | 226 | ## Test 227 | 228 | Run tests with pytest: 229 | 230 | ```sh 231 | poetry run pytest 232 | ``` 233 | 234 | Run [black](https://github.com/psf/black) linter: 235 | 236 | ```sh 237 | black . 238 | ``` 239 | -------------------------------------------------------------------------------- /flyyer/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.0" 2 | 3 | from flyyer.flyyer import Flyyer, FlyyerMeta, to_query, FlyyerRender 4 | -------------------------------------------------------------------------------- /flyyer/flyyer.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | from urllib.parse import urlencode 3 | from typing import Optional, Mapping, Union, Any 4 | from typing_extensions import TypedDict 5 | from hashlib import sha256 6 | import hmac 7 | import jwt 8 | 9 | 10 | class FlyyerMeta(TypedDict, total=False): 11 | agent: str 12 | width: Union[str, int] 13 | height: Union[str, int] 14 | resolution: Union[str, int] 15 | id: Union[str, int] 16 | v: Union[str, int] 17 | 18 | 19 | class FlyyerRender: 20 | def __init__( 21 | self, 22 | tenant: str, 23 | deck: str, 24 | template: str, 25 | version: Optional[int] = None, 26 | extension: Optional[str] = None, 27 | variables: Optional[Mapping[Any, Any]] = None, 28 | meta: Optional[FlyyerMeta] = None, 29 | secret: Optional[str] = None, 30 | strategy: Optional[str] = None, 31 | ): 32 | self.tenant = tenant 33 | self.deck = deck 34 | self.template = template 35 | self.version = version 36 | self.extension = extension 37 | self.variables = variables if variables else {} 38 | self.meta = meta if meta else {} 39 | self.secret = secret 40 | self.strategy = strategy 41 | if strategy and strategy.lower() != "hmac" and strategy.lower() != "jwt": 42 | raise Exception("Invalid `strategy`. Valid options are `HMAC` or `JWT`.") 43 | if strategy and not secret: 44 | raise Exception( 45 | "Missing `secret`. You can find it in your project in Advanced settings." 46 | ) 47 | if secret and not strategy: 48 | raise Exception( 49 | "Got `secret` but missing `strategy`. Valid options are `HMAC` or `JWT`." 50 | ) 51 | 52 | def querystring(self) -> str: 53 | default_v = { 54 | "__v": self.meta.get("v", str(int(time()))) 55 | } # This forces crawlers to refresh the image 56 | defaults_without_v = { 57 | "__id": self.meta.get("id"), 58 | "_w": self.meta.get("width"), 59 | "_h": self.meta.get("height"), 60 | "_res": self.meta.get("resolution"), 61 | "_ua": self.meta.get("agent"), 62 | } 63 | if self.strategy and self.secret: 64 | key = self.secret.encode("ASCII") 65 | if self.strategy.lower() == "hmac": 66 | data = "#".join( 67 | [ 68 | self.deck, 69 | self.template, 70 | self.version or "", 71 | self.extension or "", 72 | to_query( 73 | { 74 | **defaults_without_v, 75 | **self.variables, 76 | } 77 | ), 78 | ], 79 | ).encode("ASCII") 80 | __hmac = hmac.new(key, data, sha256).hexdigest()[:16] 81 | return to_query( 82 | { 83 | **default_v, 84 | **defaults_without_v, 85 | **self.variables, 86 | "__hmac": __hmac, 87 | } 88 | ) 89 | elif self.strategy.lower() == "jwt": 90 | jwt_defaults = { 91 | "i": self.meta.get("id"), 92 | "w": self.meta.get("width"), 93 | "h": self.meta.get("height"), 94 | "r": self.meta.get("resolution"), 95 | "u": self.meta.get("agent"), 96 | "var": self.variables, 97 | } 98 | data = { 99 | "d": self.deck, 100 | "t": self.template, 101 | "v": self.version, 102 | "e": self.extension, 103 | **jwt_defaults, 104 | } 105 | __jwt = jwt.encode(data, key, algorithm="HS256", headers=None) 106 | return to_query({"__jwt": __jwt, **default_v}) 107 | else: 108 | return to_query({**default_v, **defaults_without_v, **self.variables}) 109 | 110 | def href(self) -> str: 111 | query = self.querystring() 112 | base_href = "https://cdn.flyyer.io/render/v2" 113 | if self.strategy and self.strategy.lower() == "jwt": 114 | return f"{base_href}/{self.tenant}?{query}" 115 | final_href = f"{base_href}/{self.tenant}/{self.deck}/{self.template}" 116 | if self.version: 117 | final_href += f".{self.version}" 118 | if self.extension: 119 | final_href += f".{self.extension}" 120 | return f"{final_href}?{query}" 121 | 122 | def __str__(self): 123 | return self.href() 124 | 125 | 126 | class Flyyer: 127 | def __init__( 128 | self, 129 | project: str, 130 | path: Optional[str] = "/", 131 | secret: Optional[str] = None, 132 | strategy: Optional[str] = None, 133 | variables: Optional[Mapping[Any, Any]] = None, 134 | meta: Optional[FlyyerMeta] = None, 135 | default: Optional[str] = None, 136 | ): 137 | self.project = project 138 | self.path = path if path.startswith("/") else "/" + path 139 | self.default = default 140 | self.secret = secret 141 | self.strategy = strategy 142 | self.variables = variables if variables else {} 143 | self.meta = meta if meta else {} 144 | if strategy and strategy.lower() != "hmac" and strategy.lower() != "jwt": 145 | raise Exception("Invalid `strategy`. Valid options are `HMAC` or `JWT`.") 146 | if strategy and not secret: 147 | raise Exception( 148 | "Missing `secret`. You can find it in your project in Advanced settings." 149 | ) 150 | if secret and not strategy: 151 | raise Exception( 152 | "Got `secret` but missing `strategy`. Valid options are `HMAC` or `JWT`." 153 | ) 154 | 155 | def params_hash(self, ignoreV, isJWT=False) -> Union[str, dict]: 156 | if not isJWT: 157 | defaults = { 158 | "__v": self.meta.get( 159 | "v", str(int(time())) 160 | ), # This forces crawlers to refresh the image 161 | "__id": self.meta.get("id"), 162 | "_w": self.meta.get("width"), 163 | "_h": self.meta.get("height"), 164 | "_res": self.meta.get("resolution"), 165 | "_ua": self.meta.get("agent"), 166 | "_def": self.default, 167 | } 168 | if ignoreV: 169 | defaults.pop("__v", None) 170 | return {**defaults, **self.variables} 171 | else: 172 | jwt_defaults = { 173 | "path": self.path, 174 | "params": { 175 | "i": self.meta.get("id"), 176 | "w": self.meta.get("width"), 177 | "h": self.meta.get("height"), 178 | "r": self.meta.get("resolution"), 179 | "u": self.meta.get("agent"), 180 | "def": self.default, 181 | "var": self.variables, 182 | }, 183 | } 184 | return jwt_defaults 185 | 186 | def querystring(self, ignoreV=False) -> str: 187 | params = self.params_hash(ignoreV) 188 | aux = to_query(params).split("&") 189 | aux.sort() 190 | return "&".join(aux) 191 | 192 | def sign(self) -> str: 193 | # strategy & secret consistency checked on init 194 | if self.strategy == None: 195 | return "_" 196 | key = self.secret.encode("ASCII") 197 | if self.strategy and self.strategy.lower() == "hmac": 198 | data = (self.project + self.path + self.querystring(True)).encode("ASCII") 199 | return hmac.new(key, data, sha256).hexdigest()[:16] 200 | elif self.strategy and self.strategy.lower() == "jwt": 201 | data = {k: v for k, v in self.params_hash(True, True).items() if v is not None} 202 | return jwt.encode(data, key, algorithm="HS256", headers=None) 203 | 204 | def href(self) -> str: 205 | signature = self.sign() 206 | if self.strategy and self.strategy.lower() == "jwt": 207 | final_version = self.meta.get("v", str(int(time()))) 208 | return f"https://cdn.flyyer.io/v2/{self.project}/jwt-{signature}?__v={final_version}" 209 | else: 210 | query = self.querystring() 211 | return f"https://cdn.flyyer.io/v2/{self.project}/{signature}/{query}{self.path}" 212 | 213 | def __str__(self): 214 | return self.href() 215 | 216 | 217 | # From https://stackoverflow.com/a/43347067/3416691 218 | # Alternative: https://stackoverflow.com/a/4014164/3416691 219 | def to_query(params: Mapping[Any, Any]) -> str: 220 | g_encode_params = {} 221 | 222 | def _encode_params(params, p_key=None): 223 | encode_params = {} 224 | if params is None: 225 | pass # skip 226 | elif isinstance(params, dict): 227 | for key in params: 228 | encode_key = "{}[{}]".format(p_key, key) 229 | encode_params[encode_key] = params[key] 230 | elif isinstance(params, (list, tuple)): 231 | for offset, value in enumerate(params): 232 | encode_key = "{}[{}]".format(p_key, offset) 233 | encode_params[encode_key] = value 234 | elif isinstance(params, (bool)): 235 | g_encode_params[p_key] = str(params).lower() 236 | else: 237 | g_encode_params[p_key] = params 238 | 239 | for key in encode_params: 240 | value = encode_params[key] 241 | _encode_params(value, key) 242 | 243 | if isinstance(params, dict): 244 | for key in params: 245 | _encode_params(params[key], key) 246 | 247 | return urlencode(g_encode_params) 248 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "astroid" 11 | version = "2.4.2" 12 | description = "An abstract syntax tree for Python with inference support." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=3.5" 16 | 17 | [package.dependencies] 18 | lazy-object-proxy = ">=1.4.0,<1.5.0" 19 | six = ">=1.12,<2.0" 20 | typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} 21 | wrapt = ">=1.11,<2.0" 22 | 23 | [[package]] 24 | name = "atomicwrites" 25 | version = "1.4.0" 26 | description = "Atomic file writes." 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 30 | 31 | [[package]] 32 | name = "attrs" 33 | version = "20.3.0" 34 | description = "Classes Without Boilerplate" 35 | category = "dev" 36 | optional = false 37 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 38 | 39 | [package.extras] 40 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] 41 | docs = ["furo", "sphinx", "zope.interface"] 42 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 43 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] 44 | 45 | [[package]] 46 | name = "black" 47 | version = "20.8b1" 48 | description = "The uncompromising code formatter." 49 | category = "dev" 50 | optional = false 51 | python-versions = ">=3.6" 52 | 53 | [package.dependencies] 54 | appdirs = "*" 55 | click = ">=7.1.2" 56 | dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} 57 | mypy-extensions = ">=0.4.3" 58 | pathspec = ">=0.6,<1" 59 | regex = ">=2020.1.8" 60 | toml = ">=0.10.1" 61 | typed-ast = ">=1.4.0" 62 | typing-extensions = ">=3.7.4" 63 | 64 | [package.extras] 65 | colorama = ["colorama (>=0.4.3)"] 66 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 67 | 68 | [[package]] 69 | name = "click" 70 | version = "7.1.2" 71 | description = "Composable command line interface toolkit" 72 | category = "dev" 73 | optional = false 74 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 75 | 76 | [[package]] 77 | name = "colorama" 78 | version = "0.4.4" 79 | description = "Cross-platform colored terminal text." 80 | category = "dev" 81 | optional = false 82 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 83 | 84 | [[package]] 85 | name = "dataclasses" 86 | version = "0.6" 87 | description = "A backport of the dataclasses module for Python 3.6" 88 | category = "dev" 89 | optional = false 90 | python-versions = "*" 91 | 92 | [[package]] 93 | name = "importlib-metadata" 94 | version = "3.3.0" 95 | description = "Read metadata from Python packages" 96 | category = "dev" 97 | optional = false 98 | python-versions = ">=3.6" 99 | 100 | [package.dependencies] 101 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 102 | zipp = ">=0.5" 103 | 104 | [package.extras] 105 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 106 | testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 107 | 108 | [[package]] 109 | name = "isort" 110 | version = "5.6.4" 111 | description = "A Python utility / library to sort Python imports." 112 | category = "dev" 113 | optional = false 114 | python-versions = ">=3.6,<4.0" 115 | 116 | [package.extras] 117 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 118 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 119 | colors = ["colorama (>=0.4.3,<0.5.0)"] 120 | 121 | [[package]] 122 | name = "lazy-object-proxy" 123 | version = "1.4.3" 124 | description = "A fast and thorough lazy object proxy." 125 | category = "dev" 126 | optional = false 127 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 128 | 129 | [[package]] 130 | name = "mccabe" 131 | version = "0.6.1" 132 | description = "McCabe checker, plugin for flake8" 133 | category = "dev" 134 | optional = false 135 | python-versions = "*" 136 | 137 | [[package]] 138 | name = "more-itertools" 139 | version = "8.6.0" 140 | description = "More routines for operating on iterables, beyond itertools" 141 | category = "dev" 142 | optional = false 143 | python-versions = ">=3.5" 144 | 145 | [[package]] 146 | name = "mypy-extensions" 147 | version = "0.4.3" 148 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 149 | category = "dev" 150 | optional = false 151 | python-versions = "*" 152 | 153 | [[package]] 154 | name = "packaging" 155 | version = "20.8" 156 | description = "Core utilities for Python packages" 157 | category = "dev" 158 | optional = false 159 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 160 | 161 | [package.dependencies] 162 | pyparsing = ">=2.0.2" 163 | 164 | [[package]] 165 | name = "pathspec" 166 | version = "0.8.1" 167 | description = "Utility library for gitignore style pattern matching of file paths." 168 | category = "dev" 169 | optional = false 170 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 171 | 172 | [[package]] 173 | name = "pluggy" 174 | version = "0.13.1" 175 | description = "plugin and hook calling mechanisms for python" 176 | category = "dev" 177 | optional = false 178 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 179 | 180 | [package.dependencies] 181 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 182 | 183 | [package.extras] 184 | dev = ["pre-commit", "tox"] 185 | 186 | [[package]] 187 | name = "py" 188 | version = "1.10.0" 189 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 190 | category = "dev" 191 | optional = false 192 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 193 | 194 | [[package]] 195 | name = "pyjwt" 196 | version = "2.0.1" 197 | description = "JSON Web Token implementation in Python" 198 | category = "main" 199 | optional = false 200 | python-versions = ">=3.6" 201 | 202 | [package.extras] 203 | crypto = ["cryptography (>=3.3.1,<4.0.0)"] 204 | dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1,<4.0.0)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"] 205 | docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] 206 | tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] 207 | 208 | [[package]] 209 | name = "pylint" 210 | version = "2.6.0" 211 | description = "python code static checker" 212 | category = "dev" 213 | optional = false 214 | python-versions = ">=3.5.*" 215 | 216 | [package.dependencies] 217 | astroid = ">=2.4.0,<=2.5" 218 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 219 | isort = ">=4.2.5,<6" 220 | mccabe = ">=0.6,<0.7" 221 | toml = ">=0.7.1" 222 | 223 | [[package]] 224 | name = "pyparsing" 225 | version = "2.4.7" 226 | description = "Python parsing module" 227 | category = "dev" 228 | optional = false 229 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 230 | 231 | [[package]] 232 | name = "pytest" 233 | version = "5.4.3" 234 | description = "pytest: simple powerful testing with Python" 235 | category = "dev" 236 | optional = false 237 | python-versions = ">=3.5" 238 | 239 | [package.dependencies] 240 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 241 | attrs = ">=17.4.0" 242 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 243 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 244 | more-itertools = ">=4.0.0" 245 | packaging = "*" 246 | pluggy = ">=0.12,<1.0" 247 | py = ">=1.5.0" 248 | wcwidth = "*" 249 | 250 | [package.extras] 251 | checkqa-mypy = ["mypy (==v0.761)"] 252 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 253 | 254 | [[package]] 255 | name = "regex" 256 | version = "2020.11.13" 257 | description = "Alternative regular expression module, to replace re." 258 | category = "dev" 259 | optional = false 260 | python-versions = "*" 261 | 262 | [[package]] 263 | name = "six" 264 | version = "1.15.0" 265 | description = "Python 2 and 3 compatibility utilities" 266 | category = "dev" 267 | optional = false 268 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 269 | 270 | [[package]] 271 | name = "toml" 272 | version = "0.10.2" 273 | description = "Python Library for Tom's Obvious, Minimal Language" 274 | category = "dev" 275 | optional = false 276 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 277 | 278 | [[package]] 279 | name = "typed-ast" 280 | version = "1.4.1" 281 | description = "a fork of Python 2 and 3 ast modules with type comment support" 282 | category = "dev" 283 | optional = false 284 | python-versions = "*" 285 | 286 | [[package]] 287 | name = "typing-extensions" 288 | version = "3.7.4.3" 289 | description = "Backported and Experimental Type Hints for Python 3.5+" 290 | category = "main" 291 | optional = false 292 | python-versions = "*" 293 | 294 | [[package]] 295 | name = "wcwidth" 296 | version = "0.2.5" 297 | description = "Measures the displayed width of unicode strings in a terminal" 298 | category = "dev" 299 | optional = false 300 | python-versions = "*" 301 | 302 | [[package]] 303 | name = "wrapt" 304 | version = "1.12.1" 305 | description = "Module for decorators, wrappers and monkey patching." 306 | category = "dev" 307 | optional = false 308 | python-versions = "*" 309 | 310 | [[package]] 311 | name = "zipp" 312 | version = "3.4.0" 313 | description = "Backport of pathlib-compatible object wrapper for zip files" 314 | category = "dev" 315 | optional = false 316 | python-versions = ">=3.6" 317 | 318 | [package.extras] 319 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 320 | testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 321 | 322 | [metadata] 323 | lock-version = "1.1" 324 | python-versions = "^3.6" 325 | content-hash = "e4b75ec69edf45cb9faac85d246785f4b414a01c5b8e0cdb68d74c33fe31fb56" 326 | 327 | [metadata.files] 328 | appdirs = [ 329 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 330 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 331 | ] 332 | astroid = [ 333 | {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"}, 334 | {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"}, 335 | ] 336 | atomicwrites = [ 337 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 338 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 339 | ] 340 | attrs = [ 341 | {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, 342 | {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, 343 | ] 344 | black = [ 345 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 346 | ] 347 | click = [ 348 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 349 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 350 | ] 351 | colorama = [ 352 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 353 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 354 | ] 355 | dataclasses = [ 356 | {file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"}, 357 | {file = "dataclasses-0.6.tar.gz", hash = "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"}, 358 | ] 359 | importlib-metadata = [ 360 | {file = "importlib_metadata-3.3.0-py3-none-any.whl", hash = "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"}, 361 | {file = "importlib_metadata-3.3.0.tar.gz", hash = "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed"}, 362 | ] 363 | isort = [ 364 | {file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"}, 365 | {file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"}, 366 | ] 367 | lazy-object-proxy = [ 368 | {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, 369 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, 370 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"}, 371 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"}, 372 | {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"}, 373 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"}, 374 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"}, 375 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"}, 376 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"}, 377 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"}, 378 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"}, 379 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"}, 380 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"}, 381 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"}, 382 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"}, 383 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"}, 384 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"}, 385 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"}, 386 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"}, 387 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"}, 388 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, 389 | ] 390 | mccabe = [ 391 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 392 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 393 | ] 394 | more-itertools = [ 395 | {file = "more-itertools-8.6.0.tar.gz", hash = "sha256:b3a9005928e5bed54076e6e549c792b306fddfe72b2d1d22dd63d42d5d3899cf"}, 396 | {file = "more_itertools-8.6.0-py3-none-any.whl", hash = "sha256:8e1a2a43b2f2727425f2b5839587ae37093f19153dc26c0927d1048ff6557330"}, 397 | ] 398 | mypy-extensions = [ 399 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 400 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 401 | ] 402 | packaging = [ 403 | {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, 404 | {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, 405 | ] 406 | pathspec = [ 407 | {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, 408 | {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, 409 | ] 410 | pluggy = [ 411 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 412 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 413 | ] 414 | py = [ 415 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 416 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 417 | ] 418 | pyjwt = [ 419 | {file = "PyJWT-2.0.1-py3-none-any.whl", hash = "sha256:b70b15f89dc69b993d8a8d32c299032d5355c82f9b5b7e851d1a6d706dffe847"}, 420 | {file = "PyJWT-2.0.1.tar.gz", hash = "sha256:a5c70a06e1f33d81ef25eecd50d50bd30e34de1ca8b2b9fa3fe0daaabcf69bf7"}, 421 | ] 422 | pylint = [ 423 | {file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"}, 424 | {file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"}, 425 | ] 426 | pyparsing = [ 427 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 428 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 429 | ] 430 | pytest = [ 431 | {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, 432 | {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, 433 | ] 434 | regex = [ 435 | {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, 436 | {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"}, 437 | {file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"}, 438 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"}, 439 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"}, 440 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"}, 441 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"}, 442 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"}, 443 | {file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"}, 444 | {file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"}, 445 | {file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"}, 446 | {file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"}, 447 | {file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"}, 448 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"}, 449 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"}, 450 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"}, 451 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"}, 452 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"}, 453 | {file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"}, 454 | {file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"}, 455 | {file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"}, 456 | {file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"}, 457 | {file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"}, 458 | {file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"}, 459 | {file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"}, 460 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"}, 461 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"}, 462 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"}, 463 | {file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"}, 464 | {file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"}, 465 | {file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"}, 466 | {file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"}, 467 | {file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"}, 468 | {file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"}, 469 | {file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"}, 470 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"}, 471 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"}, 472 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"}, 473 | {file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"}, 474 | {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"}, 475 | {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, 476 | ] 477 | six = [ 478 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 479 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 480 | ] 481 | toml = [ 482 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 483 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 484 | ] 485 | typed-ast = [ 486 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 487 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 488 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 489 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 490 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 491 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 492 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 493 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"}, 494 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 495 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 496 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 497 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 498 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 499 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"}, 500 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 501 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 502 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 503 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 504 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 505 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"}, 506 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 507 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 508 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 509 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"}, 510 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"}, 511 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"}, 512 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"}, 513 | {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"}, 514 | {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"}, 515 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 516 | ] 517 | typing-extensions = [ 518 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 519 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 520 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 521 | ] 522 | wcwidth = [ 523 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 524 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 525 | ] 526 | wrapt = [ 527 | {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, 528 | ] 529 | zipp = [ 530 | {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, 531 | {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, 532 | ] 533 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "flyyer" 3 | version = "2.1.2" 4 | description = "FLYYER.io helper classes and methods" 5 | authors = ["Patricio López Juri ", "Franco Méndez Z "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://flyyer.io/" 9 | repository = "https://github.com/useflyyer/flyyer-python" 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.6" 13 | typing-extensions = "^3.7.4" 14 | PyJWT = "^2.0.1" 15 | 16 | [tool.poetry.dev-dependencies] 17 | pytest = "^5.2" 18 | black = {version = "*", allow-prereleases = true} 19 | pylint = "^2.5.3" 20 | 21 | [build-system] 22 | requires = ["poetry>=0.12"] 23 | build-backend = "poetry.masonry.api" 24 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/useflyyer/flyyer-python/3f3eb754aa6718654cfba131d993651b9afafad8/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_flyyer.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import unquote 2 | from re import search, match 3 | import jwt 4 | import pytest 5 | 6 | 7 | from flyyer import __version__, FlyyerMeta, to_query, Flyyer, FlyyerRender 8 | 9 | 10 | def test_version(): 11 | assert __version__ is not None 12 | 13 | 14 | def test_simple_stringify(): 15 | data = {"a": "hello", "b": 100, "c": False, "d": None, "b": 999} 16 | result = to_query(data) 17 | assert result == "a=hello&b=999&c=false" 18 | 19 | 20 | def test_complex_stringify(): 21 | data = {"a": {"aa": "bar", "ab": "foo"}, "b": [{"c": "foo"}, {"c": "bar"}]} 22 | result = to_query(data) 23 | assert unquote(result) == "a[aa]=bar&a[ab]=foo&b[0][c]=foo&b[1][c]=bar" 24 | 25 | def test_flyyer_render_url_encoding(): 26 | flyyer = FlyyerRender( 27 | tenant="tenant", 28 | deck="deck", 29 | template="template", 30 | variables={"title": "Hello world!"}, 31 | ) 32 | href = flyyer.href() 33 | assert href.startswith("https://cdn.flyyer.io/render/v2/tenant/deck/template?__v=") 34 | assert href.endswith("&title=Hello+world%21") 35 | assert href == str(flyyer) 36 | 37 | 38 | def test_flyyer_render_url_encoding_with_extension(): 39 | flyyer = FlyyerRender( 40 | tenant="tenant", 41 | deck="deck", 42 | template="template", 43 | extension="jpeg", 44 | variables={"title": "Hello world!"}, 45 | ) 46 | href = flyyer.href() 47 | assert href.startswith( 48 | "https://cdn.flyyer.io/render/v2/tenant/deck/template.jpeg?__v=" 49 | ) 50 | assert href.endswith("&title=Hello+world%21") 51 | assert href == str(flyyer) 52 | 53 | 54 | def test_flyyer_render_url_encoding_with_version(): 55 | flyyer = FlyyerRender( 56 | tenant="tenant", 57 | deck="deck", 58 | template="template", 59 | extension="png", 60 | version=12, 61 | variables={"title": "Hello world!"}, 62 | ) 63 | href = flyyer.href() 64 | assert href.startswith( 65 | "https://cdn.flyyer.io/render/v2/tenant/deck/template.12.png?__v=" 66 | ) 67 | assert href.endswith("&title=Hello+world%21") 68 | assert href == str(flyyer) 69 | 70 | 71 | def test_flyyer_render_meta_parameters(): 72 | flyyer = FlyyerRender( 73 | tenant="tenant", 74 | deck="deck", 75 | template="template", 76 | extension="jpeg", 77 | variables={"title": "title"}, 78 | meta=FlyyerMeta( 79 | agent="whatsapp", 80 | id="dev forgot to slugify", 81 | width="100", 82 | height=200, 83 | ), 84 | ) 85 | href = flyyer.href() 86 | assert href.startswith( 87 | "https://cdn.flyyer.io/render/v2/tenant/deck/template.jpeg?__v=" 88 | ) 89 | assert href.find("&title=title") != -1 90 | assert href.find("&_ua=whatsapp") != -1 91 | assert href.find("&_w=100") != -1 92 | assert href.find("&_h=200") != -1 93 | assert href.find("&__id=dev+forgot+to+slugify") != -1 94 | assert href.find("&_res=") == -1 95 | 96 | 97 | def test_flyyer_render_encode_url_with_hmac(): 98 | key = "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx" 99 | flyyer = FlyyerRender( 100 | tenant="tenant", 101 | deck="deck", 102 | template="template", 103 | extension="jpeg", 104 | variables={"title": "Hello world!"}, 105 | secret=key, 106 | strategy="HMAC", 107 | ) 108 | href = flyyer.href() 109 | assert ( 110 | match( 111 | r"https:\/\/cdn.flyyer.io\/render\/v2\/tenant\/deck\/template.jpeg\?__v=\d+&title=Hello\+world%21&__hmac=6b631ae8c4ca2977", 112 | href, 113 | ) 114 | != None 115 | ) 116 | 117 | 118 | def test_flyyer_render_encode_url_with_jwt_default_values(): 119 | key = "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx" 120 | flyyer = FlyyerRender( 121 | tenant="tenant", 122 | deck="deck", 123 | template="template", 124 | version=4, 125 | extension="jpeg", 126 | variables={"title": "Hello world!"}, 127 | secret=key, 128 | strategy="JWT", 129 | ) 130 | href = flyyer.href() 131 | token = search(r"(.*)(jwt=)(.*)", href).groups(2)[2].split("&__v=")[0] 132 | decoded = jwt.decode(token, key, algorithms=["HS256"]) 133 | check = { 134 | "d": "deck", 135 | "t": "template", 136 | "v": 4, 137 | "e": "jpeg", 138 | "i": None, 139 | "w": None, 140 | "h": None, 141 | "r": None, 142 | "u": None, 143 | "var": { 144 | "title": "Hello world!", 145 | } 146 | } 147 | assert decoded == check 148 | assert ( 149 | match( 150 | r"https:\/\/cdn.flyyer.io\/render\/v2\/tenant\?__jwt=.*?&__v=\d+", 151 | href, 152 | ) 153 | != None 154 | ) 155 | 156 | 157 | def test_flyyer_render_encode_url_with_jwt_with_meta(): 158 | key = "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx" 159 | flyyer = FlyyerRender( 160 | tenant="tenant", 161 | deck="deck", 162 | template="template", 163 | secret=key, 164 | strategy="JWT", 165 | meta=FlyyerMeta( 166 | agent="whatsapp", 167 | id="dev forgot to slugify", 168 | width="100", 169 | height=200, 170 | ), 171 | ) 172 | href = flyyer.href() 173 | token = search(r"(.*)(jwt=)(.*)", href).groups(2)[2].split("&__v=")[0] 174 | decoded = jwt.decode(token, key, algorithms=["HS256"]) 175 | check = { 176 | "d": "deck", 177 | "t": "template", 178 | "v": None, 179 | "e": None, 180 | "i": "dev forgot to slugify", 181 | "w": "100", 182 | "h": 200, 183 | "r": None, 184 | "u": "whatsapp", 185 | "var": {}, 186 | } 187 | assert decoded == check 188 | 189 | 190 | def test_flyyer_meta_parameters(): 191 | flyyer = Flyyer( 192 | project="project", 193 | path="/path/to/product", 194 | variables={"title": "title"}, 195 | meta=FlyyerMeta( 196 | agent="whatsapp", 197 | v="123123", 198 | id="dev forgot to slugify", 199 | width="100", 200 | height=200, 201 | ), 202 | ) 203 | href = flyyer.href() 204 | assert ( 205 | match( 206 | r"https:\/\/cdn.flyyer.io\/v2\/project\/_\/__id=dev\+forgot\+to\+slugify&__v=\d+&_h=200&_ua=whatsapp&_w=100&title=title\/path\/to\/product", 207 | href, 208 | ) 209 | != None 210 | ) 211 | 212 | 213 | def test_flyyer_encode_url_happy_path(): 214 | flyyer = Flyyer( 215 | project="project", 216 | path="/path/to/product", 217 | variables={"title": "Hello world!"}, 218 | meta=FlyyerMeta( 219 | id="dev forgot to slugify", 220 | width="100", 221 | height=200, 222 | ), 223 | ) 224 | href = flyyer.href() 225 | assert ( 226 | match( 227 | r"https:\/\/cdn.flyyer.io\/v2\/project\/_\/__id=dev\+forgot\+to\+slugify&__v=\d+&_h=200&_w=100&title=Hello\+world%21\/path\/to\/product", 228 | href, 229 | ) 230 | != None 231 | ) 232 | 233 | 234 | def test_flyyer_encode_url_default_values(): 235 | flyyer = Flyyer( 236 | project="project", 237 | ) 238 | href = flyyer.href() 239 | assert match(r"https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=\d+\/", href) != None 240 | 241 | 242 | def test_flyyer_encode_url_with_path_missing_slash_at_start(): 243 | flyyer = Flyyer( 244 | project="project", 245 | path="path/to/product", 246 | variables={"title": "Hello world!"}, 247 | ) 248 | href = flyyer.href() 249 | assert ( 250 | match( 251 | r"https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=\d+&title=Hello\+world%21\/path\/to\/product", 252 | href, 253 | ) 254 | != None 255 | ) 256 | 257 | 258 | def test_flyyer_encode_url_with_query_params(): 259 | flyyer = Flyyer( 260 | project="project", 261 | path="/collection/col?sort=price", 262 | variables={"title": "Hello world!"}, 263 | meta=FlyyerMeta( 264 | id="dev forgot to slugify", 265 | width="100", 266 | height=200, 267 | ), 268 | ) 269 | href = flyyer.href() 270 | assert ( 271 | match( 272 | r"https:\/\/cdn.flyyer.io\/v2\/project\/_\/__id=dev\+forgot\+to\+slugify&__v=\d+&_h=200&_w=100&title=Hello\+world%21\/collection\/col\/?\?sort=price", 273 | href, 274 | ) 275 | != None 276 | ) 277 | 278 | 279 | def test_flyyer_encode_url_with_hmac(): 280 | key = "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx" 281 | flyyer = Flyyer( 282 | project="project", 283 | path="/collections/col", 284 | secret=key, 285 | strategy="HMAC", 286 | meta=FlyyerMeta( 287 | id="dev forgot to slugify", 288 | width="100", 289 | height=200, 290 | ), 291 | variables={"title": "Hello world!"}, 292 | ) 293 | href = flyyer.href() 294 | assert ( 295 | match( 296 | r"https:\/\/cdn.flyyer.io\/v2\/project\/361b2a456daf8415\/__id=dev\+forgot\+to\+slugify&__v=\d+&_h=200&_w=100&title=Hello\+world%21\/collections\/col", 297 | href, 298 | ) 299 | != None 300 | ) 301 | 302 | 303 | def test_flyyer_encode_url_with_jwt_default_values(): 304 | key = "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx" 305 | flyyer = Flyyer( 306 | project="project", 307 | secret=key, 308 | strategy="JWT", 309 | ) 310 | href = flyyer.href() 311 | token = search(r"(.*)(jwt-)(.*)(\?__v=\d+)", href).groups(2)[2] 312 | decoded = jwt.decode(token, key, algorithms=["HS256"]) 313 | assert decoded["params"]["var"] == {} 314 | assert flyyer.path == "/" 315 | 316 | 317 | def test_flyyer_encode_url_with_jwt_with_meta(): 318 | key = "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx" 319 | flyyer = Flyyer( 320 | project="project", 321 | secret=key, 322 | strategy="JWT", 323 | meta=FlyyerMeta( 324 | id="dev forgot to slugify", 325 | width="100", 326 | height=200, 327 | ), 328 | variables={"custom": True} 329 | ) 330 | href = flyyer.href() 331 | token = search(r"(.*)(jwt-)(.*)(\?__v=\d+)", href).groups(2)[2] 332 | decoded = jwt.decode(token, key, algorithms=["HS256"]) 333 | assert decoded["params"]["i"] == "dev forgot to slugify" 334 | assert decoded["params"]["w"] == "100" 335 | assert decoded["params"]["h"] == 200 336 | assert decoded["path"] == "/" 337 | assert decoded["params"]["var"] == { "custom": True} 338 | 339 | 340 | def test_flyyer_encode_url_with_jwt_without_slash_at_start(): 341 | key = "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx" 342 | flyyer = Flyyer( 343 | project="project", 344 | secret=key, 345 | strategy="JWT", 346 | path="collections/col", 347 | meta=FlyyerMeta( 348 | id="dev forgot to slugify", 349 | width="100", 350 | height=200, 351 | ), 352 | ) 353 | href = flyyer.href() 354 | token = search(r"(.*)(jwt-)(.*)(\?__v=\d+)", href).groups(2)[2] 355 | decoded = jwt.decode(token, key, algorithms=["HS256"]) 356 | assert decoded["params"]["i"] == "dev forgot to slugify" 357 | assert decoded["params"]["w"] == "100" 358 | assert decoded["params"]["h"] == 200 359 | assert decoded["path"] == "/collections/col" 360 | 361 | def test_flyyer_encodes_default_image_param(): 362 | flyyer0 = Flyyer( 363 | project="project", 364 | path="path", 365 | default="/static/product/1.png" 366 | ) 367 | href0 = flyyer0.href() 368 | assert ( 369 | match( 370 | r"https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=\d+&_def=%2Fstatic%2Fproduct%2F1.png\/path", 371 | href0, 372 | ) 373 | != None 374 | ) 375 | flyyer1 = Flyyer( 376 | project="project", 377 | path="path", 378 | default="https://www.flyyer.io/logo.png" 379 | ) 380 | href1 = flyyer1.href() 381 | assert ( 382 | match( 383 | r"https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)&_def=https%3A%2F%2Fwww.flyyer.io%2Flogo.png\/path", 384 | href1, 385 | ) 386 | != None 387 | ) 388 | 389 | def test_flyyer_encode_url_with_jwt_and_default_image_absolute(): 390 | key = "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx" 391 | flyyer = Flyyer( 392 | project="project", 393 | secret=key, 394 | strategy="JWT", 395 | path="collections/col", 396 | variables={ 397 | "rooms": "3", 398 | "baths": "2", 399 | "sqm": 120, 400 | }, 401 | default="https://www.flyyer.io/logo.png" 402 | ) 403 | href = flyyer.href() 404 | token = search(r"(.*)(jwt-)(.*)(\?__v=\d+)", href).groups(2)[2] 405 | decoded = jwt.decode(token, key, algorithms=["HS256"]) 406 | assert decoded["params"]["var"]["sqm"] == 120 407 | assert decoded["params"]["var"]["rooms"] == "3" 408 | assert decoded["params"]["var"]["baths"] == "2" 409 | assert decoded["params"]["def"] == "https://www.flyyer.io/logo.png" 410 | assert decoded["path"] == "/collections/col" 411 | 412 | def test_flyyer_encode_url_with_jwt_and_default_image_relative(): 413 | key = "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx" 414 | flyyer = Flyyer( 415 | project="project", 416 | secret=key, 417 | strategy="JWT", 418 | path="collections/col", 419 | meta=FlyyerMeta( 420 | id="dev forgot to slugify", 421 | width="100", 422 | height=200, 423 | ), 424 | default="/logo.png" 425 | ) 426 | href = flyyer.href() 427 | token = search(r"(.*)(jwt-)(.*)(\?__v=\d+)", href).groups(2)[2] 428 | decoded = jwt.decode(token, key, algorithms=["HS256"]) 429 | assert decoded["params"]["i"] == "dev forgot to slugify" 430 | assert decoded["params"]["w"] == "100" 431 | assert decoded["params"]["h"] == 200 432 | assert decoded["params"]["def"] == "/logo.png" 433 | assert decoded["path"] == "/collections/col" 434 | 435 | def test_flyyer_wrong_jwt_key_throws(): 436 | key1 = "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx" 437 | key2 = "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0ty" 438 | flyyer = Flyyer( 439 | project="project", 440 | secret=key1, 441 | strategy="JWT", 442 | path="collections/col", 443 | ) 444 | href = flyyer.href() 445 | token = search(r"(.*)(jwt-)(.*)(\?__v=\d+)", href).groups(2)[2] 446 | assert jwt.decode(token, key1, algorithms=["HS256"]) 447 | pytest.raises(jwt.exceptions.InvalidSignatureError, jwt.decode, *[token, key2], algorithms=["HS256"]) 448 | --------------------------------------------------------------------------------