Skip to content

Commit

Permalink
Merge b49509a into e264cc8
Browse files Browse the repository at this point in the history
  • Loading branch information
cobaltine committed May 12, 2023
2 parents e264cc8 + b49509a commit 36905a0
Show file tree
Hide file tree
Showing 23 changed files with 334 additions and 338 deletions.
6 changes: 4 additions & 2 deletions .flake8
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
[flake8]
ignore = E226
max-line-length = 119
max-line-length = 150
max-complexity = 10
per-file-ignores =
json_fingerprint/tests/run.py:F401
json_fingerprint/__init__.py:F401
6 changes: 3 additions & 3 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: '3.x'
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
13 changes: 7 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,23 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.6', '3.7', '3.8', '3.9']
python-version: ['3.8', '3.9', '3.10', '3.11']

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install flake8
python -m pip install isort
python -m pip install coveralls
- name: Lint with flake8
- name: Test code formatting with flake8
run: |
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=119 --statistics
flake8 ./json_fingerprint --count --exit-zero --statistics
isort . --diff
- name: Test with unittest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
18 changes: 0 additions & 18 deletions .github/workflows/semver.yml

This file was deleted.

23 changes: 12 additions & 11 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
# Ignore everything
*

# Whitelist only project files
!.github/
# Include only project files
!**/testdata/
!**/tests/
!**/workflows/
!.gitignore
!.flake8
!.coveragerc
!.version
!*.json
!*.yml
!*.md
!*.py
!**/testdata/
!**/tests/
!json_fingerprint/
!*.yml
!.coveragerc
!.flake8
!.github/
!.gitignore
!.version
!LICENSE
!MANIFEST.in
!json_fingerprint/
!pyproject.toml
!requirements*.in
!requirements*.txt
!MANIFEST.in
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# json-fingerprint

![](https://img.shields.io/github/license/cobaltine/json-fingerprint) ![](https://img.shields.io/pypi/pyversions/json-fingerprint) ![](https://img.shields.io/github/workflow/status/cobaltine/json-fingerprint/Test%20runner/main?label=ci) ![](https://img.shields.io/github/workflow/status/cobaltine/json-fingerprint/Release%20Python%20package/main?label=cd) [![](https://img.shields.io/pypi/v/json-fingerprint)](https://pypi.org/project/json-fingerprint/) ![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/cobaltine/json-fingerprint) [![Coverage Status](https://coveralls.io/repos/github/cobaltine/json-fingerprint/badge.svg?branch=main)](https://coveralls.io/github/cobaltine/json-fingerprint?branch=main)

![](https://img.shields.io/github/license/cobaltine/json-fingerprint)
![](https://img.shields.io/pypi/pyversions/json-fingerprint)
![](https://img.shields.io/github/actions/workflow/status/cobaltine/json-fingerprint/ci.yml?branch=main&label=build)
[![](https://img.shields.io/pypi/v/json-fingerprint)](https://pypi.org/project/json-fingerprint/)
![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/cobaltine/json-fingerprint)
[![Coverage Status](https://coveralls.io/repos/github/cobaltine/json-fingerprint/badge.svg?branch=main)](https://coveralls.io/github/cobaltine/json-fingerprint?branch=main)

Create consistent and comparable fingerprints with secure hashes from unordered JSON data.

Expand Down
3 changes: 2 additions & 1 deletion json_fingerprint/_decode.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from typing import Tuple

from ._validators import _validate_fingerprint_format


def decode(fingerprint: str) -> Tuple[int, str, str]:
"""Decode json fingerprints into version, hash function and hash values."""
_validate_fingerprint_format(fingerprint)
elements = fingerprint.split('$')
elements = fingerprint.split("$")
version = int(elements[0][4:])
hash_function = elements[1]
hash = elements[2]
Expand Down
12 changes: 5 additions & 7 deletions json_fingerprint/_find_matches.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
from ._decode import decode
from typing import Dict, List

from ._create import create
from typing import (
Dict,
List,
)
from ._decode import decode


def _get_target_hashes(fingerprints: List[str]) -> List[Dict]:
target_hashes = []
for fingerprint in fingerprints:
version, hash_function, _ = decode(fingerprint=fingerprint)
element = {'version': version, 'hash_function': hash_function}
element = {"version": version, "hash_function": hash_function}
if element not in target_hashes:
target_hashes.append(element)

Expand All @@ -20,7 +18,7 @@ def _get_target_hashes(fingerprints: List[str]) -> List[Dict]:
def _create_input_fingerprints(input: str, target_hashes: List[Dict]) -> List[str]:
input_fingerprints = []
for element in target_hashes:
fingerprint = create(input=input, hash_function=element['hash_function'], version=element['version'])
fingerprint = create(input=input, hash_function=element["hash_function"], version=element["version"])
input_fingerprints.append(fingerprint)
return input_fingerprints

Expand Down
37 changes: 16 additions & 21 deletions json_fingerprint/_jfpv1.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import hashlib
import json

from typing import (
Any,
Dict,
List,
)
from typing import Any, Dict, List


def _create_json_hash(data: Any, hash_function: str) -> str:
Expand All @@ -15,17 +10,17 @@ def _create_json_hash(data: Any, hash_function: str) -> str:
allow_nan=False,
ensure_ascii=False,
indent=None,
separators=(',', ':'),
separators=(",", ":"),
skipkeys=False,
sort_keys=True,
)
if hash_function == 'sha256':
if hash_function == "sha256":
m = hashlib.sha256()
if hash_function == 'sha384':
if hash_function == "sha384":
m = hashlib.sha384()
if hash_function == 'sha512':
if hash_function == "sha512":
m = hashlib.sha512()
m.update(stringified.encode('utf-8'))
m.update(stringified.encode("utf-8"))

return m.hexdigest()

Expand All @@ -43,37 +38,37 @@ def _create_sorted_hash_list(data: Dict, hash_function: str) -> List[Dict]:
def _build_path(key: str, base_path: str):
"""Build a path string."""
if base_path:
return f'{base_path}|{key}'
return f"{base_path}|{key}"
return key


def _build_element(path: str, siblings: str, value: Any):
"""Build an element dictionary based on presence of sibling data."""
if siblings:
return {
'path': path,
'siblings': siblings,
'value': value,
"path": path,
"siblings": siblings,
"value": value,
}

return {
'path': path,
'value': value,
"path": path,
"value": value,
}


def _flatten_json(data: Dict, hash_function: str, path: str = '', siblings: List = [], debug: bool = False) -> List:
def _flatten_json(data: Dict, hash_function: str, path: str = "", siblings: List = [], debug: bool = False) -> List:
"""Flatten json data structures into a sibling-aware data element list."""
out = []
if type(data) is dict:
for key in data.keys():
p = _build_path(key=f'{{{key}}}', base_path=path)
p = _build_path(key=f"{{{key}}}", base_path=path)
output = _flatten_json(data=data[key], hash_function=hash_function, path=p, siblings=siblings, debug=debug)
out.extend(output)
return out

if type(data) is list:
p = _build_path(key=f'[{len(data)}]', base_path=path)
p = _build_path(key=f"[{len(data)}]", base_path=path)

# Iterate and collect sibling structures, which'll be then attached to each sibling element
siblings = []
Expand Down Expand Up @@ -102,4 +97,4 @@ def _create_jfpv1_fingerprint(data: Any, hash_function: str, version: int):
flattened_json = _flatten_json(data=data, hash_function=hash_function)
sorted_hash_list = _create_sorted_hash_list(data=flattened_json, hash_function=hash_function)
hex_digest = _create_json_hash(data=sorted_hash_list, hash_function=hash_function)
return f'jfpv1${hash_function}${hex_digest}'
return f"jfpv1${hash_function}${hex_digest}"
2 changes: 1 addition & 1 deletion json_fingerprint/_match.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from ._decode import decode
from ._create import create
from ._decode import decode


def match(input: str, target_fingerprint: str) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion json_fingerprint/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ def _load_json(data: str):
try:
return json.loads(data)
except Exception:
err = 'Unable to load JSON'
err = "Unable to load JSON"
raise FingerprintJSONLoadError(err) from None
29 changes: 11 additions & 18 deletions json_fingerprint/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,22 @@
FingerprintVersionError,
)

SHA256_JFP_REGEX_PATTERN = re.compile('^jfpv1\\$sha256\\$[0-9a-f]{64}$')
SHA384_JFP_REGEX_PATTERN = re.compile('^jfpv1\\$sha384\\$[0-9a-f]{96}$')
SHA512_JFP_REGEX_PATTERN = re.compile('^jfpv1\\$sha512\\$[0-9a-f]{128}$')
SHA256_JFP_REGEX_PATTERN = re.compile("^jfpv1\\$sha256\\$[0-9a-f]{64}$")
SHA384_JFP_REGEX_PATTERN = re.compile("^jfpv1\\$sha384\\$[0-9a-f]{96}$")
SHA512_JFP_REGEX_PATTERN = re.compile("^jfpv1\\$sha512\\$[0-9a-f]{128}$")

JFPV1_HASH_FUNCTIONS = (
'sha256',
'sha384',
'sha512',
"sha256",
"sha384",
"sha512",
)

JSON_FINGERPRINT_VERSIONS = (
1,
)
JSON_FINGERPRINT_VERSIONS = (1,)


def _validate_hash_function(hash_function: str, version: int):
if hash_function not in JFPV1_HASH_FUNCTIONS:
err = (f'Expected one of supported hash functions \'{JFPV1_HASH_FUNCTIONS}\', '
f'instead got \'{hash_function}\'')
err = f"Expected one of supported hash functions '{JFPV1_HASH_FUNCTIONS}', " f"instead got '{hash_function}'"
raise FingerprintHashFunctionError(err)


Expand All @@ -37,20 +34,16 @@ def _validate_input_type(input: str):

def _validate_version(version: int):
if version not in JSON_FINGERPRINT_VERSIONS:
err = (f'Expected one of supported JSON fingerprint versions \'{JSON_FINGERPRINT_VERSIONS}\', '
f'instead got \'{version}\'')
err = f"Expected one of supported JSON fingerprint versions '{JSON_FINGERPRINT_VERSIONS}', " f"instead got '{version}'"
raise FingerprintVersionError(err)


def _validate_fingerprint_format(fingerprint: str):
is_valid = False

if SHA256_JFP_REGEX_PATTERN.match(fingerprint) or \
SHA384_JFP_REGEX_PATTERN.match(fingerprint) or \
SHA512_JFP_REGEX_PATTERN.match(fingerprint):
if SHA256_JFP_REGEX_PATTERN.match(fingerprint) or SHA384_JFP_REGEX_PATTERN.match(fingerprint) or SHA512_JFP_REGEX_PATTERN.match(fingerprint):
is_valid = True

if not is_valid:
err = ('Expected JSON fingerprint in format \'{fingerprint_version}${hash_function}${hex_digest}\', instead got: '
f'{fingerprint}')
err = "Expected JSON fingerprint in format '{fingerprint_version}${hash_function}${hex_digest}', instead got: " f"{fingerprint}"
raise FingerprintStringFormatError(err)
2 changes: 1 addition & 1 deletion json_fingerprint/tests/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
from json_fingerprint.tests.test_match import TestMatch
from json_fingerprint.tests.test_validators import TestValidators

if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()
Loading

0 comments on commit 36905a0

Please sign in to comment.