Skip to content

Commit

Permalink
Merge branch '2.1.0' into 'master'
Browse files Browse the repository at this point in the history
MetaStalk 2.1.0

### What does this MR do?
#### Added

- Ability to pass both directories and individual files.
- Unittests for testing.
- Footer for run time.
- Export feature.
- Added metastalk dev and image install.
- [Codacy](https://www.codacy.com/)
- Two new arguments `--no-open` and `--alphabetic`.
  - `--no-open` will make it so a new browser tab is not opened.
  - `--alphabetic` will sort all the charts alphabetically.

#### Changed

- Created MetaStalk Class.
- All titles for charts are centered.

See merge request Cyb3r-Jak3/metastalk!14
  • Loading branch information
Cyb3r-Jak3 committed May 6, 2020
2 parents f4e57c2 + 1e87a55 commit 9298279
Show file tree
Hide file tree
Showing 26 changed files with 406 additions and 220 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,5 @@ dmypy.json
.pyre/

# Hide vscode folder
.vscode/
.vscode/
metastalk_exports/*
21 changes: 10 additions & 11 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ stages:
- lint
- test
- Test_Build
- Deploy_to_PyPi
- Deploy

include:
- template: SAST.gitlab-ci.yml
- template: Dependency-Scanning.gitlab-ci.yml
- template: License-Scanning.gitlab-ci.yml


dependency_scanning:
only:
refs:
Expand All @@ -21,7 +22,6 @@ license_scanning:
- tags
- master

# This job will fail until tests are implemented
Coverage:
image: python:3.7
stage: test
Expand All @@ -30,21 +30,22 @@ Coverage:
paths:
- MetaStalk.log
- htmlcov/*
allow_failure: true
- metastalk_exports/*
before_script:
- pip install -U pip coverage -r requirements.txt --quiet
- curl https://deepsource.io/cli | sh
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter; chmod +x ./cc-test-reporter; ./cc-test-reporter before-build
script:
- pip install dist/*.whl
- coverage run -a --source . ./MetaStalk/main.py ./ExamplePhotos/ -t -d
- coverage run -a --source . ./MetaStalk/main.py ./ExamplePhotos/Panasonic_DMC-FZ30.jpg ./ExamplePhotos/DSCN0010.jpg -t -d
- pip install .
- coverage run --source=./MetaStalk/ -m unittest -vv
after_script:
- coverage report
- coverage html
- coverage xml
- bash <(curl -Ls https://coverage.codacy.com/get.sh) report -l Python -r coverage.xml
- ./bin/deepsource report --analyzer test-coverage --key python --value-file ./coverage.xml
- ./cc-test-reporter after-build
- bash <(curl -s https://codecov.io/bash)

.lint:
stage: lint
Expand Down Expand Up @@ -119,19 +120,17 @@ python-3.8-test:
extends: ".test"
image: python:3.8


Deploy_to_PyPi:
image: python:3.7
stage: Deploy_to_PyPi
stage: Deploy
variables:
TWINE_USERNAME: $PRODUCTION_USERNAME
TWINE_USERNAME: $PYPI_USER
TWINE_PASSWORD: $PRODUCTION_PASSWORD
before_script:
- pip install -U pip setuptools twine --quiet
script:
- python setup.py sdist bdist_wheel
- python -m twine check dist/*
- python -m twine upload --verbose --username $PRODUCTION_USER --password $PRODUCTION_PASSWORD dist/*
- python -m twine upload --verbose dist/*
only:
refs:
- tags
Expand Down
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

<!-- markdownlint-disable MD024 -->

## [v2.1.0]- 2020-05-06

### Added

- Ability to pass both directories and individual files.
- Unittests for testing.
- Footer for run time.
- Export feature.
- Added metastalk dev and image install.
- [Codacy](https://www.codacy.com/)
- Two new arguments `--no-open` and `--alphabetic`.
- `--no-open` will make it so a new browser tab is not opened.
- `--alphabetic` will sort all the charts alphabetically.

### Changed

- Created MetaStalk Class.
- All titles for charts are centered.

## [v2.0.0] - 2020-05-03

## Rename to MetaStalk
Expand All @@ -11,7 +30,7 @@ Rename to MetaStalk to create PyPi package and a lot backend development changes
### Added

- License scanning
- [Codecov](https://codecov.io/gl/Cyb3r-Jak3/pystalk)
- [Codecov](https://codecov.io/gl/Cyb3r-Jak3/metastalk)
- Pipfile and Pipfile.lock for pipenv.
- Added .gitlab folder for service desk.
- setup.py for pypi.
Expand Down
14 changes: 10 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@

Thanks for reading this because I am always looking to collaborate with other people to see their ideas.

If you are looking to submit a pull request then please do so on [GitLab](https://gitlab.com/Cyb3r-Jak3/pystalk) because all development is done there. Any pull request that is opened on GitHub will be closed and I mirror it on GitLab.
If you are looking to submit a pull request then please do so on [GitLab](https://gitlab.com/Cyb3r-Jak3/MetaStalk) because all development is done there. Any pull request that is opened on GitHub will be closed and I mirror it on GitLab.

Please open issues on [GitLab](https://gitlab.com/Cyb3r-Jak3/PyStalk/issues). if you do not have a GitLab account, send then please email service desk( [[email protected]](mailto:[email protected]) ).
Please open issues on [GitLab](https://gitlab.com/Cyb3r-Jak3/MetaStalk/issues). If you do not have a GitLab account, send then please email service desk( [incoming+cyb3r-jak3-metastalk-15263483-issue-@incoming.gitlab.com](mailto:incoming+cyb3r-jak3-metastalk-15263483-issue-@incoming.gitlab.com)).

To get started you can either clone the repo or use `pip install metastalk[dev]`.

## Testing

Currently the tests at [tests](tests/) cover all the necessary items to make sure the it works. Currently no test exist for testing the image export capabilities as it relies on [orca](https://github.com/plotly/orca) which is not easy to setup tests for. If someone has a way of creating a test with orca, then please submit a pull request and I will gladly approve.

## Modules

Expand All @@ -13,10 +19,10 @@ If you are looking to write a new module for a new metadata graph here is they a
- Each module is written in the [modules](modules/) directory and is added to the [\_\_init__.py](modules/__init__.py) in the directory.
- A list is passed to the modules called `photos` which containing dictionaries with all the metadata points for each photo.
- The module will find the key that is using and return a figure using plotly.
- A dictionary of all the plots will be passed to [web.py](utils/web.py) which will use dash to display them.
- A dictionary of all the plots will be passed to [web.py](utils/web.py) which will use Dash to display them.

### Example image dictionary

```python
{'Image width': '1600 pixels', 'Image height': '1200 pixels', 'Image orientation': 'Horizontal (normal)', 'Bits/pixel': '24', 'Pixel format': 'YCbCr', 'Creation date': '2007-11-29 16:16:21', 'Camera aperture': '2.97', 'Camera focal': '2.8', 'Camera exposure': '1/6', 'Camera model': 'Canon PowerShot SD300', 'Camera manufacturer': 'Canon', 'Compression': 'JPEG (Baseline)', 'Thumbnail size': '5922 bytes', 'EXIF version': '0220', 'Date-time original': '2007-11-29 16:16:21', 'Date-time digitized': '2007-11-29 16:16:21', 'Compressed bits per pixel': '3', 'Shutter speed': '2.59', 'Aperture': '2.97', 'Exposure bias': '0', 'Focal length': '5.8', 'Flashpix version': '0100', 'Focal plane width': '7.14e+03', 'Focal plane height': '7.14e+03', 'Comment': 'JPEG quality: 90% (approximate)', 'MIME type': 'image/jpeg', 'Endianness': 'Big endian', 'item': '.\\utils\\ExamplePhotos\\22-canon_tags.jpg'}
{'Image width': '1600 pixels', 'Image height': '1200 pixels', 'Image orientation': 'Horizontal (normal)', 'Bits/pixel': '24', 'Pixel format': 'YCbCr', 'Creation date': '2007-11-29 16:16:21', 'Camera aperture': '2.97', 'Camera focal': '2.8', 'Camera exposure': '1/6', 'Camera model': 'Canon PowerShot SD300', 'Camera manufacturer': 'Canon', 'Compression': 'JPEG (Baseline)', 'Thumbnail size': '5922 bytes', 'EXIF version': '0220', 'Date-time original': '2007-11-29 16:16:21', 'Date-time digitized': '2007-11-29 16:16:21', 'Compressed bits per pixel': '3', 'Shutter speed': '2.59', 'Aperture': '2.97', 'Exposure bias': '0', 'Focal length': '5.8', 'Flashpix version': '0100', 'Focal plane width': '7.14e+03', 'Focal plane height': '7.14e+03', 'Comment': 'JPEG quality: 90% (approximate)', 'MIME type': 'image/jpeg', 'Endianness': 'Big endian', 'item': '.\\ExamplePhotos\\22-canon_tags.jpg'}
```
Binary file added ExamplePhotos/corrupt_image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions Manifest.in
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
graft MetaStalk/utils/assets
prune MetaStalk/tests
recursive-include MetaStalk/utils/assets *
4 changes: 2 additions & 2 deletions MetaStalk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""MetaStalk
"""MetaStalk.
---
MetaStalk is a program that parse image metadata and creates charts from it.
"""
__version__ = "v2.0.0"
__version__ = "v2.1.0"
__author__ = "Cyb3r Jak3"
190 changes: 86 additions & 104 deletions MetaStalk/main.py
Original file line number Diff line number Diff line change
@@ -1,135 +1,117 @@
# -*- coding: utf-8 -*-
"""This script get the exif data from photos
and creates graphs from the metadata"""
"""Main function of MetaStalk.
Run get any metadata from photos
and creates graphs from the metadata using MetaStalk.Modules"""
import argparse
from collections import OrderedDict
import os
import logging
import timeit
from hachoir.parser import createParser
from hachoir.metadata import extractMetadata


from hachoir.core import config as hachoirconfig
import MetaStalk.utils as utils
import MetaStalk.modules as modules

hachoirconfig.quiet = True

t_start = timeit.default_timer()

class MetaStalk():
"""MetaStalk.
---
Main Class for all MetaStalk work
"""
def __init__(self):
self.log = logging.getLogger("MetaStalk")
self.t_start = timeit.default_timer()
self.valid, self.invalid = [], []
self.plots = {}

def run(self, args):
"""Run
Process files and generates graphs
Arguments:
args {argparse.Namespace} -- The arguments from start()
log {logging.Logger} -- Logger
"""
for path in args.files:
if os.path.isdir(path):
self.log.debug("Detected path as a directory")
for item in os.listdir(path):
item_path = os.path.join(path, item)
self.file_search(item_path)
else:
self.file_search(path)

self.plots = {
"Stats": modules.stats(self.valid, self.invalid),
"GPS": modules.gps_check(self.valid),
"Timestamp": modules.date_time(self.valid),
"Model": modules.pie_chart(self.valid, "Camera model"),
"Manufacturer": modules.pie_chart(self.valid, "Camera manufacturer"),
"Focal": modules.pie_chart(self.valid, "Camera focal"),
"Producer": modules.pie_chart(self.valid, "Producer")
}
if args.alphabetic:
self.plots = OrderedDict(sorted(self.plots.items()))
if args.export:
utils.export(args.export, args.output, self.plots)
utils.graph(self.plots, self.t_start, args.test, args.no_open)

def file_search(self, parse_file: str):
"""file_search
Used to append files if the path is not a directory.
Arguments
files {str} -- Name of the file to parse.
"""
parser = createParser(parse_file)
try:
metadata = extractMetadata(parser).exportDictionary()["Metadata"]
metadata["item"] = parse_file
self.valid.append(metadata)
self.log.debug("%s has metadata", parse_file)
except AttributeError:
self.invalid.append(parse_file)
self.log.debug("%s has no metadata data", parse_file)

def start():
"""start

Sets up MetaStalk and parses arguments.
Will raise an IOError if no path is passed.
def start():
"""
Function needed to start MetaStalk
"""
parser = argparse.ArgumentParser(prog="MetaStalk",
description="Tool to graph "
"image metadata.")
parser.add_argument('files', nargs='*', default=None,
help='Path of photos to check.')
parser.add_argument('-t', '--test', default=False, action="store_true",
help='Does not show the graphs at the end.')
# Logging function from https://stackoverflow.com/a/20663028
parser.add_argument("-a", "--alphabetic", help="Sorts charts in alphabetical order rather than"
" the default order", default=False, action="store_true")
parser.add_argument('-d', '--debug', help="Sets logging level to DEBUG.",
action="store_const", dest="loglevel",
const=logging.DEBUG, default=logging.WARNING)
parser.add_argument("-e", "--export", choices=["pdf", "svg", "png", "html"],
help="Exports the graphs rather than all on one webpage")
parser.add_argument("--no-open", help="Will only start the server and not open the browser"
" to view it", default=False, action="store_true")
parser.add_argument("-o", "--output", default="metastalk_exports",
help="The name of the directory to output exports to. "
"Will be created if it does not exist. "
"Defaults to metastalk_exports.")
parser.add_argument('-t', '--test', default=False, action="store_true",
help='Does not show the graphs at the end.')
parser.add_argument("-v", "--verbose", help="Sets logging level to INFO",
action="store_const", dest="loglevel",
const=logging.INFO)
args = parser.parse_args()

log = utils.make_logger("MetaStalk", args.loglevel)
log.info("MetaStalk starting")
if not args.files:
log.error("ERROR: No path was inputted.")
raise IOError("No path was inputted.")
run(args, log)


def run(args, log: logging.Logger):
"""run
Process files and generates graphs
Arguments:
args {argparse.Namespace} -- The arguments from start()
log {logging.Logger} -- Logger
"""
for path in args.files:
isdir = os.path.isdir(path)
log.debug("Detected path as a directory")

if isdir:
photos, invalid_photos = directory_search(args.files[0], log)
else:
photos, invalid_photos = file_search(args.files, log)

plots = {
"STATS": modules.Stats(photos, invalid_photos),
"GPS": modules.GPS_Check(photos),
"Timestamp": modules.date_time(photos),
"Model": modules.PieChart(photos, "Camera model"),
"Manufacturer": modules.PieChart(photos, "Camera manufacturer"),
"Focal": modules.PieChart(photos, "Camera focal"),
"Producer": modules.PieChart(photos, "Producer")
}

utils.graph(plots, t_start, args.test)


def directory_search(files: list, log: logging.Logger) -> (list, list):
"""directory_search
Used to append all files in a directory from args.
Arguments:
files {list} -- List of directories to parse
log {logging.Logger} -- Logger
Returns:
valid, invalid -- List of photos with metadata and ones without
"""
valid, invalid = [], []
for item in os.listdir(files):
item_path = os.path.join(files, item)
parser = createParser(item_path)
metadata = extractMetadata(parser).exportDictionary()["Metadata"]
if metadata:
metadata["item"] = item_path
valid.append(metadata)
log.debug("%s has metadata", item)
else:
metadata["item"] = item_path
invalid.append(metadata)
log.debug("%s has no metadata", item)
return valid, invalid


def file_search(files: list, log: logging.Logger) -> (list, list):
"""file_search
Used to append files if the path is not a directory.
Arguments:
files {list} -- List of files to parse
log {logging.Logger} -- Logger
Returns:
valid, invalid -- List of photos with metadata and ones without
"""
valid, invalid = [], []
for _, item in enumerate(files):
parser = createParser(item)
metadata = extractMetadata(parser).exportDictionary()["Metadata"]
if metadata:
metadata["item"] = item
valid.append(metadata)
log.debug("%s has metadata", item)
else:
metadata["item"] = item
invalid.append(metadata)
log.debug("%s has no metadata data", item)
return valid, invalid


start()
raise FileNotFoundError("No path was inputted.")
metastalk = MetaStalk()
metastalk.run(args)
Loading

0 comments on commit 9298279

Please sign in to comment.