Skip to content

Commit 665284f

Browse files
authored
Feature/content type enhancement (ghandic#58)
* Adding JWT, plain, zip, gzip, jpg, webp generation for content type
1 parent 7ffd0dd commit 665284f

File tree

16 files changed

+313
-86
lines changed

16 files changed

+313
-86
lines changed

.github/workflows/python-package.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jobs:
5757
pip install .
5858
PACKAGE_DIR=`pip show jsf | grep "Location" | sed 's/^.*: //'`
5959
cd $PACKAGE_DIR/jsf
60-
pip install pytest
60+
pip install pytest pyjwt
6161
pytest
6262
- name: Upload coverage
6363
uses: codecov/codecov-action@v1

jsf/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ Navigate to [http://127.0.0.1:8000](http://127.0.0.1:8000) and check out your en
147147

148148
</div>
149149

150+
### Partially supported features
151+
152+
- string `contentMediaType` - only a subset of these are supported, however they can be expanded within [this file](jsf/schema_types/string_utils/content_type/__init__.py)
153+
150154
## Credits
151155

152156
- This repository is a Python port of [json-schema-faker](https://github.com/json-schema-faker/json-schema-faker) with some minor differences in implementation.

jsf/schema_types/string.py

Lines changed: 14 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import base64
21
import logging
3-
import quopri
42
import random
53
import re
64
from datetime import timezone
7-
from enum import Enum
85
from typing import Any, Callable, Dict, Optional
96

107
import rstr
118
from faker import Faker
129

1310
from jsf.schema_types.base import BaseSchema, ProviderNotSetException
11+
from jsf.schema_types.string_utils import content_encoding, content_type
12+
from jsf.schema_types.string_utils.content_type.text__plain import random_fixed_length_sentence
1413

1514
logger = logging.getLogger()
1615
faker = Faker()
@@ -19,11 +18,6 @@
1918
URI_PATTERN = f"https?://{{hostname}}(?:{FRAGMENT})+"
2019
PARAM_PATTERN = "(?:\\?([a-z]{1,7}(=\\w{1,5})?&){0,3})?"
2120

22-
LOREM = """Lorem ipsum dolor sit amet consectetur adipisicing elit.
23-
Hic molestias, esse veniam placeat officiis nobis architecto modi
24-
possimus reiciendis accusantium exercitationem quas illum libero odit magnam,
25-
reprehenderit ipsum, repellendus culpa!""".split()
26-
2721

2822
def temporal_duration(
2923
positive: bool = True,
@@ -123,100 +117,35 @@ def fake_duration():
123117
}
124118

125119

126-
def random_fixed_length_sentence(_min: int, _max: int) -> str:
127-
output = ""
128-
while len(output) < _max:
129-
remaining = _max - len(output)
130-
valid_words = list(filter(lambda s: len(s) < remaining, LOREM))
131-
if len(valid_words) == 0:
132-
break
133-
output += random.choice(valid_words) + " "
134-
if len(output) > _min and random.uniform(0, 1) > 0.9:
135-
break
136-
return output.strip()
137-
138-
139-
class ContentEncoding(str, Enum):
140-
SEVEN_BIT = "7-bit"
141-
EIGHT_BIT = "8-bit"
142-
BINARY = "binary"
143-
QUOTED_PRINTABLE = "quoted-printable"
144-
BASE16 = "base-16"
145-
BASE32 = "base-32"
146-
BASE64 = "base-64"
147-
148-
149-
def binary_encoder(string: str) -> str:
150-
return "".join(format(x, "b") for x in bytearray(string, "utf-8"))
151-
152-
153-
def bytes_str_repr(b: bytes) -> str:
154-
return repr(b)[2:-1]
155-
156-
157-
def seven_bit_encoder(string: str) -> str:
158-
return bytes_str_repr(string.encode("utf-7"))
159-
160-
161-
def eight_bit_encoder(string: str) -> str:
162-
return bytes_str_repr(string.encode("utf-8"))
163-
164-
165-
def quoted_printable_encoder(string: str) -> str:
166-
return bytes_str_repr(quopri.encodestring(string.encode("utf-8")))
167-
168-
169-
def b16_encoder(string: str) -> str:
170-
return bytes_str_repr(base64.b16encode(string.encode("utf-8")))
171-
172-
173-
def b32_encoder(string: str) -> str:
174-
return bytes_str_repr(base64.b32encode(string.encode("utf-8")))
175-
176-
177-
def b64_encoder(string: str) -> str:
178-
return bytes_str_repr(base64.b64encode(string.encode("utf-8")))
179-
180-
181-
Encoder = {
182-
ContentEncoding.SEVEN_BIT: seven_bit_encoder,
183-
ContentEncoding.EIGHT_BIT: eight_bit_encoder,
184-
ContentEncoding.BINARY: binary_encoder,
185-
ContentEncoding.QUOTED_PRINTABLE: quoted_printable_encoder,
186-
ContentEncoding.BASE16: b16_encoder,
187-
ContentEncoding.BASE32: b32_encoder,
188-
ContentEncoding.BASE64: b64_encoder,
189-
}
190-
191-
192-
def encode(string: str, encoding: Optional[ContentEncoding]) -> str:
193-
return Encoder.get(encoding, lambda s: s)(string)
194-
195-
196120
class String(BaseSchema):
197121
minLength: Optional[float] = 0
198122
maxLength: Optional[float] = 50
199123
pattern: Optional[str] = None
200124
format: Optional[str] = None
201125
# enum: Optional[List[Union[str, int, float]]] = None # NOTE: Not used - enums go to enum class
202-
# contentMediaType: Optional[str] = None # TODO: Long list, need to document which ones will be supported and how to extend
203-
contentEncoding: Optional[ContentEncoding]
204-
# contentSchema # No docs detailing this yet...
126+
contentMediaType: Optional[str] = None
127+
contentEncoding: Optional[content_encoding.ContentEncoding]
128+
# contentSchema # Doesnt help with generation
205129

206130
def generate(self, context: Dict[str, Any]) -> Optional[str]:
207131
try:
208132
s = super().generate(context)
209-
return str(encode(s, self.contentEncoding)) if s else s
133+
return str(content_encoding.encode(s, self.contentEncoding)) if s else s
210134
except ProviderNotSetException:
211135
format_map["regex"] = lambda: rstr.xeger(self.pattern)
212136
format_map["relative-json-pointer"] = lambda: random.choice(
213137
context["state"]["__all_json_paths__"]
214138
)
215139
if format_map.get(self.format) is not None:
216-
return encode(format_map[self.format](), self.contentEncoding)
140+
return content_encoding.encode(format_map[self.format](), self.contentEncoding)
217141
if self.pattern is not None:
218-
return encode(rstr.xeger(self.pattern), self.contentEncoding)
219-
return encode(
142+
return content_encoding.encode(rstr.xeger(self.pattern), self.contentEncoding)
143+
if self.contentMediaType is not None:
144+
return content_encoding.encode(
145+
content_type.generate(self.contentMediaType, self.minLength, self.maxLength),
146+
self.contentEncoding,
147+
)
148+
return content_encoding.encode(
220149
random_fixed_length_sentence(self.minLength, self.maxLength), self.contentEncoding
221150
)
222151

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python_sources(name="src")
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import base64
2+
import quopri
3+
from enum import Enum
4+
from typing import Optional
5+
6+
7+
class ContentEncoding(str, Enum):
8+
SEVEN_BIT = "7-bit"
9+
EIGHT_BIT = "8-bit"
10+
BINARY = "binary"
11+
QUOTED_PRINTABLE = "quoted-printable"
12+
BASE16 = "base-16"
13+
BASE32 = "base-32"
14+
BASE64 = "base-64"
15+
16+
17+
def binary_encoder(string: str) -> str:
18+
return "".join(format(x, "b") for x in bytearray(string, "utf-8"))
19+
20+
21+
def bytes_str_repr(b: bytes) -> str:
22+
return repr(b)[2:-1]
23+
24+
25+
def seven_bit_encoder(string: str) -> str:
26+
return bytes_str_repr(string.encode("utf-7"))
27+
28+
29+
def eight_bit_encoder(string: str) -> str:
30+
return bytes_str_repr(string.encode("utf-8"))
31+
32+
33+
def quoted_printable_encoder(string: str) -> str:
34+
return bytes_str_repr(quopri.encodestring(string.encode("utf-8")))
35+
36+
37+
def b16_encoder(string: str) -> str:
38+
return bytes_str_repr(base64.b16encode(string.encode("utf-8")))
39+
40+
41+
def b32_encoder(string: str) -> str:
42+
return bytes_str_repr(base64.b32encode(string.encode("utf-8")))
43+
44+
45+
def b64_encoder(string: str) -> str:
46+
return bytes_str_repr(base64.b64encode(string.encode("utf-8")))
47+
48+
49+
Encoder = {
50+
ContentEncoding.SEVEN_BIT: seven_bit_encoder,
51+
ContentEncoding.EIGHT_BIT: eight_bit_encoder,
52+
ContentEncoding.BINARY: binary_encoder,
53+
ContentEncoding.QUOTED_PRINTABLE: quoted_printable_encoder,
54+
ContentEncoding.BASE16: b16_encoder,
55+
ContentEncoding.BASE32: b32_encoder,
56+
ContentEncoding.BASE64: b64_encoder,
57+
}
58+
59+
60+
def encode(string: str, encoding: Optional[ContentEncoding]) -> str:
61+
return Encoder.get(encoding, lambda s: s)(string)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python_sources(name="src")
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from jsf.schema_types.string_utils.content_type.application__gzip import create_random_gzip
2+
from jsf.schema_types.string_utils.content_type.application__jwt import create_random_jwt
3+
from jsf.schema_types.string_utils.content_type.application__zip import create_random_zip
4+
from jsf.schema_types.string_utils.content_type.image__jpeg import random_jpg
5+
from jsf.schema_types.string_utils.content_type.image__webp import random_webp
6+
from jsf.schema_types.string_utils.content_type.text__plain import random_fixed_length_sentence
7+
8+
9+
def not_implemented(*args, **kwargs):
10+
raise NotImplementedError()
11+
12+
13+
ContentTypeGenerator = {
14+
"application/jwt": create_random_jwt,
15+
# "text/html": not_implemented,
16+
# "application/xml": not_implemented, # To implement: Port code from https://onlinerandomtools.com/generate-random-xml
17+
# "image/bmp": not_implemented, # To implement: request jpg and convert to bmp
18+
# "text/css": not_implemented,
19+
# "text/csv": not_implemented,
20+
# "image/gif": not_implemented, # To implement: request jpg and convert to gif
21+
"image/jpeg": random_jpg,
22+
# "application/json": not_implemented, # To implement: Port code from https://onlinerandomtools.com/generate-random-xml
23+
# "text/javascript": not_implemented,
24+
# "image/png": not_implemented, # To implement: request jpg and convert to png
25+
# "image/tiff": not_implemented, # To implement: request jpg and convert to tiff
26+
"text/plain": random_fixed_length_sentence,
27+
"image/webp": random_webp,
28+
"application/zip": create_random_zip,
29+
"application/gzip": create_random_gzip,
30+
# "application/x-bzip": not_implemented, # To implement: create in memory random files using text/plain then zip
31+
# "application/x-bzip2": not_implemented, # To implement: create in memory random files using text/plain then zip
32+
# "application/pdf": not_implemented, # To implement: request jpg and convert to pdf and/or make pdf using python package
33+
# "text/calendar": not_implemented,
34+
}
35+
36+
37+
def generate(content_type: str, min_length: int, max_length: int) -> str:
38+
return ContentTypeGenerator.get(content_type, not_implemented)(min_length, max_length)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import gzip
2+
import io
3+
4+
from jsf.schema_types.string_utils.content_encoding import bytes_str_repr
5+
from jsf.schema_types.string_utils.content_type.application__zip import create_random_file_name
6+
from jsf.schema_types.string_utils.content_type.text__plain import random_fixed_length_sentence
7+
8+
9+
def create_random_gzip(*args, **kwargs) -> str:
10+
fgz = io.BytesIO()
11+
gzip_obj = gzip.GzipFile(filename=create_random_file_name(), mode="wb", fileobj=fgz)
12+
gzip_obj.write(random_fixed_length_sentence().encode("utf-8"))
13+
gzip_obj.close()
14+
15+
fgz.seek(0)
16+
return bytes_str_repr(fgz.getvalue())
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import base64
2+
import hashlib
3+
import hmac
4+
import json
5+
import secrets
6+
from datetime import timezone
7+
8+
from faker import Faker
9+
10+
faker = Faker()
11+
12+
13+
def base64url_encode(input: bytes):
14+
return base64.urlsafe_b64encode(input).decode("utf-8").replace("=", "")
15+
16+
17+
def jwt(api_key, expiry, api_sec):
18+
19+
segments = []
20+
21+
header = {"typ": "JWT", "alg": "HS256"}
22+
payload = {"iss": api_key, "exp": expiry}
23+
24+
json_header = json.dumps(header, separators=(",", ":")).encode()
25+
json_payload = json.dumps(payload, separators=(",", ":")).encode()
26+
27+
segments.append(base64url_encode(json_header))
28+
segments.append(base64url_encode(json_payload))
29+
30+
signing_input = ".".join(segments).encode()
31+
key = api_sec.encode()
32+
signature = hmac.new(key, signing_input, hashlib.sha256).digest()
33+
34+
segments.append(base64url_encode(signature))
35+
36+
encoded_string = ".".join(segments)
37+
38+
return encoded_string
39+
40+
41+
def create_random_jwt(*args, **kwargs):
42+
43+
api_key = secrets.token_urlsafe(16)
44+
api_sec = secrets.token_urlsafe(16)
45+
46+
expiry = int(faker.date_time(timezone.utc).timestamp())
47+
48+
return jwt(api_key, expiry, api_sec)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import io
2+
import random
3+
import zipfile
4+
from typing import Tuple
5+
6+
import rstr
7+
8+
from jsf.schema_types.string_utils.content_encoding import bytes_str_repr
9+
from jsf.schema_types.string_utils.content_type.text__plain import random_fixed_length_sentence
10+
11+
12+
def create_random_file_name() -> str:
13+
return rstr.xeger(r"[a-zA-Z0-9]+\.txt")
14+
15+
16+
def create_random_file() -> Tuple[str, io.BytesIO]:
17+
return (create_random_file_name(), io.BytesIO(random_fixed_length_sentence().encode("utf-8")))
18+
19+
20+
def create_random_zip(*args, **kwargs) -> str:
21+
zip_buffer = io.BytesIO()
22+
23+
with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
24+
for file_name, data in [create_random_file() for _ in range(random.randint(1, 10))]:
25+
zip_file.writestr(file_name, data.getvalue())
26+
27+
return bytes_str_repr(zip_buffer.getvalue())

0 commit comments

Comments
 (0)