diff --git a/.gitignore b/.gitignore index fcd26bd..10e3ab5 100644 --- a/.gitignore +++ b/.gitignore @@ -116,3 +116,4 @@ demo.svg htmlcov/ **/*.svg +!anybadge/templates/*.svg diff --git a/.travis.yml b/.travis.yml index 3c43788..80c19ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,16 @@ dist: xenial language: python python: -- '2.7' +- '3.4' - '3.5' - '3.6' - '3.7' +- '3.8' +- '3.9' install: - pip install -U setuptools pip -r build-requirements.txt script: -- pytest --doctest-modules --cov=anybadge --cov-report html:htmlcov anybadge.py tests +- pytest --doctest-modules --cov=anybadge --cov-report html:htmlcov anybadge tests before_deploy: - sed -i "s/^version = .*/version = __version__ = \"$TRAVIS_TAG\"/" anybadge.py deploy: @@ -21,4 +23,4 @@ deploy: on: tags: true all_branches: true - python: '3.7' \ No newline at end of file + python: '3.9' \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5e7db17 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,64 @@ +# Contributing to anybadge +I love your input! I want to make contributing to this project as easy and transparent as possible, whether it's: + +- Reporting a bug +- Discussing the current state of the code +- Submitting a fix +- Proposing new features +- Becoming a maintainer + +## I use [Github Flow](https://docs.github.com/en/get-started/quickstart/github-flow), so all code changes happen through pull requests +Pull requests are the best way to propose changes to the codebase (I use +[Github Flow](https://docs.github.com/en/get-started/quickstart/github-flow)). I actively welcome your pull requests: + +1. Fork the repo and create your branch from `master` +2. If you've added code that should be tested, add tests +3. If you've changed APIs, update the documentation +4. Ensure the test suite passes +5. Make sure your code lints (tbc) +6. Issue that pull request! + +## Any contributions you make will be under the MIT Software License +When you submit code changes, your submissions are understood to be under the same +[MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers +if that's a concern. + +## Report bugs using Github's [issues](https://github.com/jongracecox/anybadge/issues) +I use GitHub issues to track public bugs. Report a bug by +[opening a new issue](https://github.com/jongracecox/anybadge/issues/new/choose). + +## Write bug reports with detail, background, and sample code +**Great Bug Reports** tend to have: + +- A quick summary and/or background +- Steps to reproduce + - Be specific! + - Give sample code if you can (ideally sample code that *anyone* with a basic setup can run to reproduce) +- What you expected would happen - (include explanation, screenshot, drawings, etc. to be exact) +- What actually happens +- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) + +People *love* thorough bug reports. + +## Use a Consistent Coding Style +Please follow the existing coding style. + +## License +By contributing, you agree that your contributions will be licensed under its MIT License. + +# Technical stuff + +## Documentation +The `README.md` file contains a table showing example badges for the different built-in colors. If you modify the +appearance of badges, or the available colors please update the table using the following code: + +```python +import anybadge +print("""| Color Name | Hex Code | Example | +| ---------- | -------- | ------- |""") +for color in sorted([c for c in anybadge.colors.Color], key=lambda x: x.name): + file = 'examples/color_' + color.name.lower() + '.svg' + url = 'https://cdn.rawgit.com/jongracecox/anybadge/master/' + file + anybadge.Badge(label='Color', value=color, default_color=color.name.lower()).write_badge(file, overwrite=True) + print("| {color} | {hex} | ![]({url}) |".format(color=color.name.lower(), hex=color.value.upper(), url=url)) +``` diff --git a/README.md b/README.md index 76bb923..7c298a2 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Python project for generating badges for your projects [![buymeacoffee](https://camo.githubusercontent.com/c3f856bacd5b09669157ed4774f80fb9d8622dd45ce8fdf2990d3552db99bd27/68747470733a2f2f7777772e6275796d6561636f666665652e636f6d2f6173736574732f696d672f637573746f6d5f696d616765732f6f72616e67655f696d672e706e67)](https://www.buymeacoffee.com/jongracecox) +Supports: Python 3.4-3.9 (2.7 support has been dropped) + ## Overview `anybadge` can be used to add badge generation to your Python projects, @@ -140,12 +142,12 @@ Available named colors are: | aqua | #00FFFF | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_aqua.svg) | | black | #000000 | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_black.svg) | | blue | #0000FF | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_blue.svg) | -| brightred | #FF0000 | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_brightred.svg) | -| brightyellow | #FFFF00 | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_brightyellow.svg) | +| bright_red | #FF0000 | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_bright_red.svg) | +| bright_yellow | #FFFF00 | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_bright_yellow.svg) | | fuchsia | #FF00FF | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_fuchsia.svg) | | gray | #808080 | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_gray.svg) | | green | #4C1 | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_green.svg) | -| lightgrey | #9F9F9F | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_lightgrey.svg) | +| light_grey | #9F9F9F | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_light_grey.svg) | | lime | #00FF00 | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_lime.svg) | | maroon | #800000 | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_maroon.svg) | | navy | #000080 | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_navy.svg) | @@ -157,19 +159,7 @@ Available named colors are: | teal | #008080 | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_teal.svg) | | white | #FFFFFF | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_white.svg) | | yellow | #DFB317 | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_yellow.svg) | -| yellowgreen | #A4A61D | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_yellowgreen.svg) | - -This table was generated with the following code: - -```python -print("""| Color Name | Hex Code | Example | -| ---------- | -------- | ------- |""") -for color, hex in sorted(anybadge.COLORS.items()): - file = 'examples/color_' + color + '.svg' - url = 'https://cdn.rawgit.com/jongracecox/anybadge/master/' + file - anybadge.Badge(label='Color', value=color, default_color=color).write_badge(file, overwrite=True) - print("| {color} | {hex} | ![]({url}) |".format(color=color, hex=hex.upper(), url=url)) -``` +| yellow_green | #A4A61D | ![](https://cdn.rawgit.com/jongracecox/anybadge/master/examples/color_yellow_green.svg) | ### Semantic version support @@ -261,60 +251,9 @@ test1 = Badge( test1.write_badge('test1.svg') ``` -### Options +### Command-line options -These are the command line options: - -``` -positional arguments: - args Pairs of =. For example 2=red 4=orange - 6=yellow 8=good. Read this as "Less than 2 = red, less - than 4 = orange...". - -optional arguments: - -h, --help show this help message and exit - -l LABEL, --label LABEL - The badge label. - -v VALUE, --value VALUE - The badge value. - -m VALUE_FORMAT, --value-format VALUE_FORMAT - Formatting string for value (e.g. "%.2f" for 2dp - floats) - -c COLOR, --color COLOR - For fixed color badges use --colorto specify the badge - color. - -p PREFIX, --prefix PREFIX - Optional prefix for value. - -s SUFFIX, --suffix SUFFIX - Optional suffix for value. - -d PADDING, --padding PADDING - Number of characters to pad on either side of the - badge text. - -lp LABEL_PADDING, --label-padding LABEL_PADDING - Number of characters to pad on either side of the - badge label. - -vp VALUE_PADDING, --value-padding VALUE_PADDING - Number of characters to pad on either side of the - badge value. - -n FONT, --font FONT "DejaVu Sans,Verdana,Geneva,sans-serif"Font name. - Supported fonts: ,"Arial, Helvetica, sans-serif" - -z FONT_SIZE, --font-size FONT_SIZE - Font size. - -t TEMPLATE, --template TEMPLATE - Location of alternative template .svg file. - -s STYLE, --style STYLE - Alternative style of badge to create. Valid values are - "gitlab-scoped", "default". This overrides any templates - passed using --template. - -u, --use-max Use the maximum threshold color when the value exceeds - the maximum threshold. - -f FILE, --file FILE Output file location. - -o, --overwrite Overwrite output file if it already exists. - -r TEXT_COLOR, --text-color TEXT_COLOR - Text color. Single value affects both labeland value - colors. A comma separated pair affects label and value - text respectively. -``` +The command line options can be viewed using `anybadge --help`. Examples -------- @@ -344,330 +283,10 @@ anybadge.py --label=pipeline --value=passing --file=pipeline.svg passing=green f Python usage ============ -Here is the output of ``help(anybadge)``:: -``` -Help on module anybadge: - -NAME - anybadge - anybadge - -DESCRIPTION - A Python module for generating badges for your projects, with a focus on - simplicity and flexibility. - -CLASSES - builtins.object - Badge - - class Badge(builtins.object) - | Badge(label, value, font_name=None, font_size=None, num_padding_chars=None, num_label_padding_chars=None, num_value_padding_chars=None, template=None, style=None, value_prefix='', value_suffix='', thresholds=None, default_color=None, use_max_when_value_exceeds=True, value_format=None, text_color=None, semver=False) - | - | Badge class used to generate badges. - | - | Args: - | label(str): Badge label text. - | value(str): Badge value text. - | font_name(str, optional): Name of font to use. - | font_size(int, optional): Font size. - | num_padding_chars(float, optional): Number of padding characters to use to give extra - | space around text. - | num_label_padding_chars(float, optional): Number of padding characters to use to give extra - | space around label text. - | num_value_padding_chars(float, optional): Number of padding characters to use to give extra - | space around value text. - | template(str, optional): String containing the SVG template. This should be valid SVG - | file content with place holders for variables to be populated during rendering. - | style(str, optional): Style of badge to create. This will make anybadge render a badge in a - | different style. Valid values are "gitlab-scoped", "default". Default is "default". - | value_prefix(str, optional): Prefix to be placed before value. - | value_suffix(str, optional): Suffix to be placed after value. - | thresholds(dict, optional): A dictionary containing thresholds used to select badge - | color based on the badge value. - | default_color(str, optional): Badge color as a name or as an HTML color code. - | use_max_when_value_exceeds(bool, optional): Choose whether to use the maximum threshold - | value when the badge value exceeds the top threshold. Default is True. - | value_format(str, optional) String with formatting to be used to format the value text. - | text_color(str, optional): Text color as a name or as an HTML color code. - | semver(bool, optional): Used to indicate that the value is a semantic version number. - | - | Examples: - | - | Create a simple green badge: - | - | >>> badge = Badge('label', 123, default_color='green') - | - | Write a badge to file, overwriting any existing file: - | - | >>> badge = Badge('label', 123, default_color='green') - | >>> badge.write_badge('demo.svg', overwrite=True) - | - | Here are a number of examples showing thresholds, since there - | are certain situations that may not be obvious: - | - | >>> badge = Badge('pipeline', 'passing', thresholds={'passing': 'green', 'failing': 'red'}) - | >>> badge.badge_color - | 'green' - | - | 2.32 is not <2 - | 2.32 is < 4, so 2.32 yields orange - | >>> badge = Badge('pylint', 2.32, thresholds={2: 'red', - | ... 4: 'orange', - | ... 8: 'yellow', - | ... 10: 'green'}) - | >>> badge.badge_color - | 'orange' - | - | 8 is not <8 - | 8 is <4, so 8 yields orange - | >>> badge = Badge('pylint', 8, thresholds={2: 'red', - | ... 4: 'orange', - | ... 8: 'yellow', - | ... 10: 'green'}) - | >>> badge.badge_color - | 'green' - | - | 10 is not <8, but use_max_when_value_exceeds defaults to - | True, so 10 yields green - | >>> badge = Badge('pylint', 11, thresholds={2: 'red', - | ... 4: 'orange', - | ... 8: 'yellow', - | ... 10: 'green'}) - | >>> badge.badge_color - | 'green' - | - | 11 is not <10, and use_max_when_value_exceeds is set to - | False, so 11 yields the default color '#4c1' - | >>> badge = Badge('pylint', 11, use_max_when_value_exceeds=False, - | ... thresholds={2: 'red', 4: 'orange', 8: 'yellow', - | ... 10: 'green'}) - | >>> badge.badge_color - | '#4c1' - | - | Methods defined here: - | - | __init__(self, label, value, font_name=None, font_size=None, num_padding_chars=None, num_label_padding_chars=None, num_value_padding_chars=None, template=None, style=None, value_prefix='', value_suffix='', thresholds=None, default_color=None, use_max_when_value_exceeds=True, value_format=None, text_color=None, semver=False) - | Constructor for Badge class. - | - | __repr__(self) - | Return a representation of the Badge object instance. - | - | The output of the __repr__ function could be used to recreate the current object. - | - | Examples: - | - | >>> badge = Badge('example', '123.456') - | >>> repr(badge) - | "Badge('example', '123.456')" - | - | >>> badge = Badge('example', '123.456', value_suffix='TB') - | >>> repr(badge) - | "Badge('example', '123.456', value_suffix='TB')" - | - | >>> badge = Badge('example', '123.456', text_color='#111111', value_suffix='TB') - | >>> repr(badge) - | "Badge('example', '123.456', value_suffix='TB', text_color='#111111')" - | - | >>> badge = Badge('example', '123', num_padding_chars=5) - | >>> repr(badge) - | "Badge('example', '123', num_padding_chars=5)" - | - | >>> badge = Badge('example', '123', num_label_padding_chars=5) - | >>> repr(badge) - | "Badge('example', '123', num_label_padding_chars=5)" - | - | >>> badge = Badge('example', '123', num_label_padding_chars=5, num_value_padding_chars=6, - | ... template='template.svg', value_prefix='$', thresholds={10: 'green', 30: 'red'}, - | ... default_color='red', use_max_when_value_exceeds=False, value_format="%s m/s") - | >>> repr(badge) - | "Badge('example', '123', num_label_padding_chars=5, num_value_padding_chars=6, template='template.svg', value_prefix='$', thresholds={10: 'green', 30: 'red'}, default_color='red', use_max_when_value_exceeds=False, value_format='%s m/s')" - | - | __str__(self) - | Return string representation of badge. - | - | This will return the badge SVG text. - | - | Returns: str - | - | Examples: - | - | >>> print(Badge('example', '123')) # doctest: +ELLIPSIS - | - | ... - | - | get_text_width(self, text) - | Return the width of text. - | - | Args: - | text(str): Text to get the pixel width of. - | - | Returns: - | int: Pixel width of the given text based on the badges selected font. - | - | This implementation assumes a fixed font of: - | - | font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11" - | >>> badge = Badge('x', 1, font_name='DejaVu Sans,Verdana,Geneva,sans-serif', font_size=11) - | >>> badge.get_text_width('pylint') - | 34 - | - | write_badge(self, file_path, overwrite=False) - | Write badge to file. - | - | ---------------------------------------------------------------------- - | Readonly properties defined here: - | - | arc_start - | The position where the arc on the gitlab-scoped should start. - | - | Returns: int - | - | Examples: - | - | >>> badge = Badge('pylint', '5') - | >>> badge.arc_start - | 58 - | - | badge_color - | Badge color based on the configured thresholds. - | - | Returns: str - | - | badge_color_code - | Return the color code for the badge. - | - | Returns: str - | - | Raises: ValueError when an invalid badge color is set. - | - | badge_svg_text - | The badge SVG text. - | - | Returns: str - | - | badge_width - | The total width of badge. - | - | Returns: int - | - | Examples: - | - | >>> badge = Badge('pylint', '5') - | >>> badge.badge_width - | 61 - | - | color_split_position - | The SVG x position where the color split should occur. - | - | Returns: int - | - | float_thresholds - | Thresholds as a dict using floats as keys. - | - | font_width - | Return the width multiplier for a font. - | - | Returns: - | int: Maximum pixel width of badges selected font. - | - | Example: - | - | >>> Badge(label='x', value='1').font_width - | 10 - | - | label_anchor - | The SVG x position of the middle anchor for the label text. - | - | Returns: float - | - | label_anchor_shadow - | The SVG x position of the label shadow anchor. - | - | Returns: float - | - | label_width - | The SVG width of the label text. - | - | Returns: int - | - | semver_thresholds - | Thresholds as a dict using LooseVersion as keys. - | - | semver_version - | The semantic version represented by the value string. - | - | Returns: LooseVersion - | - | value_anchor - | The SVG x position of the middle anchor for the value text. - | - | Returns: float - | - | value_anchor_shadow - | The SVG x position of the value shadow anchor. - | - | Returns: float - | - | value_box_width - | The SVG width of the value text box. - | - | Returns: int - | - | value_is_float - | Identify whether the value text is a float. - | - | Returns: bool - | - | value_is_int - | Identify whether the value text is an int. - | - | Returns: bool - | - | value_type - | The Python type associated with the value. - | - | Returns: type - | - | value_width - | The SVG width of the value text. - | - | Returns: int - | - | ---------------------------------------------------------------------- - | Data descriptors defined here: - | - | __dict__ - | dictionary for instance variables (if defined) - | - | __weakref__ - | list of weak references to the object (if defined) - -FUNCTIONS - main(args=None) - Generate a badge based on command line arguments. - - parse_args(args) - Parse the command line arguments. - -DATA - BADGE_TEMPLATES = {'coverage': {'label': 'coverage', 'suffix': '%', 't... - COLORS = {'aqua': '#00FFFF', 'black': '#000000', 'blue': '#0000FF', 'b... - DEFAULT_COLOR = '#4c1' - DEFAULT_FONT = 'DejaVu Sans,Verdana,Geneva,sans-serif' - DEFAULT_FONT_SIZE = 11 - DEFAULT_TEXT_COLOR = '#fff' - FONT_WIDTHS = {'Arial, Helvetica, sans-serif': {11: 8}, 'DejaVu Sans,V... - MASK_ID_PREFIX = 'anybadge_' - NUM_PADDING_CHARS = 0.5 - TEMPLATE_GITLAB_SCOPED_SVG = '\n... - TEMPLATE_SVG = '\n - - - - - - - - - - - - - - - {{ label }} - {{ label }} - - - {{ value }} - {{ value }} - -""" - -# Template SVG for GitLab Scoped label and value badges with placeholders for -# various items that will be added during final creation. -TEMPLATE_GITLAB_SCOPED_SVG = """ - - - - - - - - - - - - - - - - {{ label }} - - - {{ value }} - -""" - -# Define some templates that can be used for common badge types, saving -# from having to provide thresholds and labels each time. -BADGE_TEMPLATES = { - 'pylint': { - 'threshold': '2=red 4=orange 8=yellow 10=green', - 'label': 'pylint' - }, - 'coverage': { - 'threshold': '50=red 60=orange 80=yellow 100=green', - 'label': 'coverage', - 'suffix': '%' - } -} - - -class Badge(object): +class Badge: """ Badge class used to generate badges. @@ -234,34 +107,32 @@ class Badge(object): """Constructor for Badge class.""" # Set defaults if values were not passed if not font_name: - font_name = DEFAULT_FONT + font_name = config.DEFAULT_FONT if not font_size: - font_size = DEFAULT_FONT_SIZE + font_size = config.DEFAULT_FONT_SIZE if num_label_padding_chars is None: if num_padding_chars is None: - num_label_padding_chars = NUM_PADDING_CHARS + num_label_padding_chars = config.NUM_PADDING_CHARS else: num_label_padding_chars = num_padding_chars if num_value_padding_chars is None: if num_padding_chars is None: - num_value_padding_chars = NUM_PADDING_CHARS + num_value_padding_chars = config.NUM_PADDING_CHARS else: num_value_padding_chars = num_padding_chars if not template: - template = TEMPLATE_SVG + template = get_template('default') if style not in ['gitlab-scoped']: style = "default" if not default_color: - default_color = DEFAULT_COLOR + default_color = config.DEFAULT_COLOR if not text_color: - text_color = DEFAULT_TEXT_COLOR + text_color = config.DEFAULT_TEXT_COLOR self.label = label self.value = value self.value_is_version = semver - if self.value_is_version and not VERSION_COMPARISON_SUPPORTED: - raise RuntimeError("Version comparison is not supported.") self.value_format = value_format if value_format: @@ -272,8 +143,11 @@ class Badge(object): self.value_suffix = value_suffix self.value_text = value_prefix + value_text + value_suffix - if font_name not in FONT_WIDTHS: - raise ValueError('Font name "%s" not found. Available fonts: %s' % (font_name, ', '.join(FONT_WIDTHS.keys()))) + if font_name not in config.FONT_WIDTHS: + raise ValueError( + 'Font name "%s" not found. ' + 'Available fonts: %s' % (font_name, ', '.join(config.FONT_WIDTHS.keys())) + ) self.font_name = font_name self.font_size = font_size self.num_label_padding_chars = num_label_padding_chars @@ -329,19 +203,19 @@ class Badge(object): """ optional_args = "" - if self.font_name != DEFAULT_FONT: + if self.font_name != config.DEFAULT_FONT: optional_args += ", font_name=%s" % repr(self.font_name) - if self.font_size != DEFAULT_FONT_SIZE: + if self.font_size != config.DEFAULT_FONT_SIZE: optional_args += ", font_size=%s" % repr(self.font_size) if self.num_label_padding_chars == self.num_value_padding_chars: - if self.num_label_padding_chars != NUM_PADDING_CHARS: + if self.num_label_padding_chars != config.NUM_PADDING_CHARS: optional_args += ", num_padding_chars=%s" % repr(self.num_label_padding_chars) else: - if self.num_label_padding_chars != NUM_PADDING_CHARS: + if self.num_label_padding_chars != config.NUM_PADDING_CHARS: optional_args += ", num_label_padding_chars=%s" % repr(self.num_label_padding_chars) - if self.num_value_padding_chars != NUM_PADDING_CHARS: + if self.num_value_padding_chars != config.NUM_PADDING_CHARS: optional_args += ", num_value_padding_chars=%s" % repr(self.num_value_padding_chars) - if self.template != TEMPLATE_SVG: + if self.template != get_template('default'): optional_args += ", template=%s" % repr(self.template) if self.style != 'default': optional_args += ", style=%s" % repr(self.style) @@ -351,13 +225,13 @@ class Badge(object): optional_args += ", value_suffix=%s" % repr(self.value_suffix) if self.thresholds: optional_args += ", thresholds=%s" % repr(self.thresholds) - if self.default_color != DEFAULT_COLOR: + if self.default_color != config.DEFAULT_COLOR: optional_args += ", default_color=%s" % repr(self.default_color) if not self.use_max_when_value_exceeds: optional_args += ", use_max_when_value_exceeds=%s" % repr(self.use_max_when_value_exceeds) if self.value_format: optional_args += ", value_format=%s" % repr(self.value_format) - if self.text_color != DEFAULT_TEXT_COLOR: + if self.text_color != config.DEFAULT_TEXT_COLOR: optional_args += ", text_color=%s" % repr(self.text_color) return "%s(%s, %s%s)" % ( @@ -375,7 +249,6 @@ class Badge(object): """ return self.badge_svg_text - @classmethod def _get_next_mask_id(cls): """Return a new mask ID from a singleton sequence maintained on the class. @@ -387,7 +260,7 @@ class Badge(object): cls.mask_id += 1 - return MASK_ID_PREFIX + str(cls.mask_id) + return config.MASK_ID_PREFIX + str(cls.mask_id) def _get_svg_template(self): """Return the correct SVG template to render, based on the style and template @@ -396,10 +269,16 @@ class Badge(object): Returns: str """ if self.style == "gitlab-scoped": - return TEMPLATE_GITLAB_SCOPED_SVG + return get_template('gitlab_scoped') # Identify whether template is a file or the actual template text + if len(self.template.split('\n')) == 1: + try: + return get_template(self.template) + except UnknownBadgeTemplate: + pass + with open(self.template, mode='r') as file_handle: return file_handle.read() else: @@ -409,16 +288,16 @@ class Badge(object): def semver_version(self): """The semantic version represented by the value string. - Returns: LooseVersion + Returns: Version """ - return LooseVersion(self.value) + return Version(self.value) @property def semver_thresholds(self): - """Thresholds as a dict using LooseVersion as keys.""" - # LooseVersion is not a hashable type, so can't be used to create an + """Thresholds as a dict using Version as keys.""" + # Version is not a hashable type, so can't be used to create an # ordered dict directly. First we need to create an ordered list of keys - ordered_keys = sorted(self.thresholds.keys(), key=LooseVersion) + ordered_keys = sorted(self.thresholds.keys(), key=Version) return OrderedDict((key, self.thresholds[key]) for key in ordered_keys) @property @@ -441,7 +320,7 @@ class Badge(object): try: _ = float(self.value) - except ValueError: + except (ValueError, TypeError): return False else: return True @@ -455,7 +334,7 @@ class Badge(object): try: a = float(self.value) b = int(self.value) - except ValueError: + except (ValueError, TypeError): return False else: return a == b @@ -467,7 +346,7 @@ class Badge(object): Returns: type """ if self.value_is_version: - return LooseVersion + return Version if self.value_is_float: return float elif self.value_is_int: @@ -511,7 +390,7 @@ class Badge(object): >>> Badge(label='x', value='1').font_width 10 """ - return FONT_WIDTHS[self.font_name][self.font_size] + return config.FONT_WIDTHS[self.font_name][self.font_size] @property def color_split_position(self): @@ -658,7 +537,7 @@ class Badge(object): # Set value and thresholds based on the value type. This will result in either # value and thresholds as floats or value and thresholds as semantic versions. - if self.value_type == LooseVersion: + if self.value_type == Version: value = self.semver_version thresholds = self.semver_thresholds else: @@ -682,7 +561,7 @@ class Badge(object): return self.default_color @property - def badge_color_code(self): + def badge_color_code(self) -> str: """Return the color code for the badge. Returns: str @@ -690,13 +569,36 @@ class Badge(object): Raises: ValueError when an invalid badge color is set. """ color = self.badge_color + + if isinstance(color, Color): + return color.value + if color[0] == '#': return color + color = color.upper() + + prefixes = ["BRIGHT", "YELLOW", "LIGHT"] + try: - return COLORS[color] + return Color[color.upper()].value except KeyError: - raise ValueError('Invalid color code "%s". Valid color codes are: %s', (color, ", ".join(COLORS.keys()))) + pass + + # For backward compatibility with old color names (that were lowercase and didn't + # contain underscores) we will try to get the same color. + + for prefix in prefixes: + if color.startswith(prefix) and color != prefix and '_' not in color: + try: + return Color[color.replace(prefix, prefix + '_')].value + except KeyError: + pass + + raise ValueError( + 'Invalid color code "%s". ' + 'Valid color codes are: %s', (color, ", ".join(list(Color.__members__.keys()))) + ) def write_badge(self, file_path, overwrite=False): """Write badge to file.""" @@ -716,248 +618,3 @@ class Badge(object): with open(path, mode='w') as file_handle: file_handle.write(self.badge_svg_text) - - -# Based on the following SO answer: https://stackoverflow.com/a/16008023/6252525 -def _get_approx_string_width(text, font_width, fixed_width=False): - """ - Get the approximate width of a string using a specific average font width. - - Args: - text(str): Text string to calculate width of. - font_width(int): Average width of font characters. - fixed_width(bool): Indicates that the font is fixed width. - - Returns: - int: Width of string in pixels. - - Examples: - - Call the function with a string and the maximum character width of the font you are using: - - >>> int(_get_approx_string_width('hello', 10)) - 29 - - This example shows the comparison of simplistic calculation based on a fixed width. - Given a test string and a fixed font width of 10, we can calculate the width - by multiplying the length and the font character with: - - >>> test_string = 'GOOGLE|ijkl' - >>> _get_approx_string_width(test_string, 10, fixed_width=True) - 110 - - Since some characters in the string are thinner than others we expect that the - apporximate text width will be narrower than the fixed width calculation: - - >>> _get_approx_string_width(test_string, 10) - 77 - - """ - if fixed_width: - return len(text) * font_width - - size = 0.0 - - # A dictionary containing percentages that relate to how wide - # each character will be represented in a variable width font. - # These percentages can be calculated using the ``_get_character_percentage_dict`` function. - char_width_percentages = { - "lij|' ": 40.0, - '![]fI.,:;/\\t': 50.0, - '`-(){}r"': 60.0, - '*^zcsJkvxy': 70.0, - 'aebdhnopqug#$L+<>=?_~FZT0123456789': 70.0, - 'BSPEAKVXY&UwNRCHD': 70.0, - 'QGOMm%W@': 100.0 - } - - for s in text: - percentage = 100.0 - for k in char_width_percentages.keys(): - if s in k: - percentage = char_width_percentages[k] - break - size += (percentage / 100.0) * float(font_width) - - return int(size) - -# This is a helper function that can be used to generate alternate dictionaries -# for the _get_approx_string_width function. The function is not needed for -# normal operation of this package, and since it depends on the PIL package, -# which is not included in the dependencies the function will remain commented out. -# -# def _get_character_percentage_dict(font_path, font_size): -# """Get the dictionary used to estimate variable width font text lengths. -# -# Args: -# font_path(str): Path to valid font file. -# font_size(int): Font size to use. -# -# Returns: dict -# -# This function can be used to calculate the dictionary used in the -# ``get_approx_string_width`` function. -# -# Examples: -# >>> _get_character_percentage_dict('/Library/Fonts/Verdana.ttf', 9) # doctest: +ELLIPSIS -# {"lij|' ": 40, '![]fI.,:;/\\\\t': 50, '`-(){}r"': 60, '*^zcsJkvxy': 70, ... -# """ -# from PIL import ImageFont -# -# # List of groups in size order, smallest to largest -# char_width_groups = [ -# "lij|' ", -# '![]fI.,:;/\\t', -# '`-(){}r"', -# '*^zcsJkvxy', -# 'aebdhnopqug#$L+<>=?_~FZT' + digits, -# 'BSPEAKVXY&UwNRCHD', -# 'QGOMm%W@', -# ] -# -# def get_largest_in_group(group): -# """Get the widest character from the group.""" -# return max([ImageFont.truetype(font_path, font_size).getsize(c)[0] for c in group]) -# -# largest = char_width_groups[-1] -# font_width = get_largest_in_group(largest) -# return {group: int((get_largest_in_group(group) / font_width) * 100) -# for group in char_width_groups} - - -def parse_args(args): - """Parse the command line arguments.""" - parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, - description=textwrap.dedent('''\ -Command line utility to generate .svg badges. - -This utility can be used to generate .svg badge images, using configurable -thresholds for coloring. Values can be passed as string, integer or floating -point. The type will be detected automatically. - -Running the utility with a --file option will result in the .svg image being -written to file. Without the --file option the .svg file content will be -written to stdout, so can be redirected to a file. - -Some thresholds have been built in to save time. To use these thresholds you -can simply specify the template name instead of threshold value/color pairs. - -examples: - - Here are some usage specific examples that may save time on defining - thresholds. - - Pylint - anybadge.py --value=2.22 --file=pylint.svg pylint - - anybadge.py --label=pylint --value=2.22 --file=pylint.svg \\ - 2=red 4=orange 8=yellow 10=green - - Coverage - anybadge.py --value=65 --file=coverage.svg coverage - - anybadge.py --label=coverage --value=65 --suffix='%%' --file=coverage.svg \\ - 50=red 60=orange 80=yellow 100=green - - CI Pipeline - anybadge.py --label=pipeline --value=passing --file=pipeline.svg \\ - passing=green failing=red - -''')) - parser.add_argument('-l', '--label', type=str, help='The badge label.') - parser.add_argument('-v', '--value', type=str, help='The badge value.', required=True) - parser.add_argument('-m', '--value-format', type=str, default=None, - help='Formatting string for value (e.g. "%%.2f" for 2dp floats)') - parser.add_argument('-c', '--color', type=str, help='For fixed color badges use --color' - 'to specify the badge color.', - default=DEFAULT_COLOR) - parser.add_argument('-p', '--prefix', type=str, help='Optional prefix for value.', - default='') - parser.add_argument('-s', '--suffix', type=str, help='Optional suffix for value.', - default='') - parser.add_argument('-d', '--padding', type=int, help='Number of characters to pad on ' - 'either side of the badge text.', - default=NUM_PADDING_CHARS) - parser.add_argument('-lp', '--label-padding', type=int, help='Number of characters to pad on ' - 'either side of the badge label.', default=None) - parser.add_argument('-vp', '--value-padding', type=int, help='Number of characters to pad on ' - 'either side of the badge value.', default=None) - parser.add_argument('-n', '--font', type=str, - help='Font name. Supported fonts: ' - ','.join(['"%s"' % x for x in FONT_WIDTHS.keys()]), - default=DEFAULT_FONT) - parser.add_argument('-z', '--font-size', type=int, help='Font size.', - default=DEFAULT_FONT_SIZE) - parser.add_argument('-t', '--template', type=str, help='Location of alternative ' - 'template .svg file.', - default=TEMPLATE_SVG) - parser.add_argument('-st', '--style', type=str, help='Alternative style of badge to create. Valid ' - 'values are "gitlab-scoped", "default". This ' - 'overrides any templates passed using --template.') - parser.add_argument('-u', '--use-max', action='store_true', - help='Use the maximum threshold color when the value exceeds the ' - 'maximum threshold.') - parser.add_argument('-f', '--file', type=str, help='Output file location.') - parser.add_argument('-o', '--overwrite', action='store_true', - help='Overwrite output file if it already exists.') - parser.add_argument('-r', '--text-color', type=str, help='Text color. Single value affects both label' - 'and value colors. A comma separated pair ' - 'affects label and value text respectively.', - default=DEFAULT_TEXT_COLOR) - parser.add_argument('-e', '--semver', action='store_true', default=False, - help='Treat value and thresholds as semantic versions.') - parser.add_argument('args', nargs=argparse.REMAINDER, help='Pairs of =. ' - 'For example 2=red 4=orange 6=yellow 8=good. ' - 'Read this as "Less than 2 = red, less than 4 = orange...".') - return parser.parse_args(args) - - -def main(args=None): - """Generate a badge based on command line arguments.""" - - # Args may be sent from command line of as args directly. - if not args: - args = sys.argv[1:] - - # Parse command line arguments - args = parse_args(args) - - label = args.label - threshold_text = args.args - suffix = args.suffix - - # Check whether thresholds were sent as one word, and is in the - # list of templates. If so, swap in the template. - if len(args.args) == 1 and args.args[0] in BADGE_TEMPLATES: - template_name = args.args[0] - template_dict = BADGE_TEMPLATES[template_name] - threshold_text = template_dict['threshold'].split(' ') - if not args.label: - label = template_dict['label'] - if not args.suffix and 'suffix' in template_dict: - suffix = template_dict['suffix'] - - if not label: - raise ValueError('Label has not been set. Please use --label argument.') - - # Create threshold list from args - threshold_list = [x.split('=') for x in threshold_text] - threshold_dict = {x[0]: x[1] for x in threshold_list} - - # Create badge object - badge = Badge(label, args.value, value_prefix=args.prefix, value_suffix=suffix, - default_color=args.color, num_padding_chars=args.padding, - num_label_padding_chars=args.label_padding, num_value_padding_chars=args.value_padding, - font_name=args.font, font_size=args.font_size, template=args.template, style=args.style, - use_max_when_value_exceeds=args.use_max, thresholds=threshold_dict, - value_format=args.value_format, text_color=args.text_color, semver=args.semver) - - if args.file: - # Write badge SVG to file - badge.write_badge(args.file, overwrite=args.overwrite) - else: - print(badge.badge_svg_text) - - -if __name__ == '__main__': - main() diff --git a/anybadge/cli.py b/anybadge/cli.py new file mode 100644 index 0000000..8a64234 --- /dev/null +++ b/anybadge/cli.py @@ -0,0 +1,146 @@ +import argparse +import sys +import textwrap + +from anybadge.styles import Style +from anybadge.templates import get_template +from . import config +from .badge import Badge + + +def parse_args(args): + """Parse the command line arguments.""" + parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, + description=textwrap.dedent('''\ +Command line utility to generate .svg badges. + +This utility can be used to generate .svg badge images, using configurable +thresholds for coloring. Values can be passed as string, integer or floating +point. The type will be detected automatically. + +Running the utility with a --file option will result in the .svg image being +written to file. Without the --file option the .svg file content will be +written to stdout, so can be redirected to a file. + +Some thresholds have been built in to save time. To use these thresholds you +can simply specify the template name instead of threshold value/color pairs. + +examples: + + Here are some usage specific examples that may save time on defining + thresholds. + + Pylint + anybadge.py --value=2.22 --file=pylint.svg pylint + + anybadge.py --label=pylint --value=2.22 --file=pylint.svg \\ + 2=red 4=orange 8=yellow 10=green + + Coverage + anybadge.py --value=65 --file=coverage.svg coverage + + anybadge.py --label=coverage --value=65 --suffix='%%' --file=coverage.svg \\ + 50=red 60=orange 80=yellow 100=green + + CI Pipeline + anybadge.py --label=pipeline --value=passing --file=pipeline.svg \\ + passing=green failing=red + +''')) + parser.add_argument('-l', '--label', type=str, help='The badge label.') + parser.add_argument('-v', '--value', type=str, help='The badge value.', required=True) + parser.add_argument('-m', '--value-format', type=str, default=None, + help='Formatting string for value (e.g. "%%.2f" for 2dp floats)') + parser.add_argument('-c', '--color', type=str, help='For fixed color badges use --color' + 'to specify the badge color.', + default=config.DEFAULT_COLOR) + parser.add_argument('-p', '--prefix', type=str, help='Optional prefix for value.', + default='') + parser.add_argument('-s', '--suffix', type=str, help='Optional suffix for value.', + default='') + parser.add_argument('-d', '--padding', type=int, help='Number of characters to pad on ' + 'either side of the badge text.', + default=config.NUM_PADDING_CHARS) + parser.add_argument('-lp', '--label-padding', type=int, help='Number of characters to pad on ' + 'either side of the badge label.', default=None) + parser.add_argument('-vp', '--value-padding', type=int, help='Number of characters to pad on ' + 'either side of the badge value.', default=None) + parser.add_argument('-n', '--font', type=str, + help='Font name. Supported fonts: ' + ','.join(['"%s"' % x for x in config.FONT_WIDTHS.keys()]), + default=config.DEFAULT_FONT) + parser.add_argument('-z', '--font-size', type=int, help='Font size.', + default=config.DEFAULT_FONT_SIZE) + parser.add_argument('-t', '--template', type=str, help='Location of alternative ' + 'template .svg file.', + default=get_template('default')) + parser.add_argument('-st', '--style', type=str, help='Alternative style of badge to create. Valid ' + 'values are "gitlab-scoped", "default". This ' + 'overrides any templates passed using --template.') + parser.add_argument('-u', '--use-max', action='store_true', + help='Use the maximum threshold color when the value exceeds the ' + 'maximum threshold.') + parser.add_argument('-f', '--file', type=str, help='Output file location.') + parser.add_argument('-o', '--overwrite', action='store_true', + help='Overwrite output file if it already exists.') + parser.add_argument('-r', '--text-color', type=str, help='Text color. Single value affects both label' + 'and value colors. A comma separated pair ' + 'affects label and value text respectively.', + default=config.DEFAULT_TEXT_COLOR) + parser.add_argument('-e', '--semver', action='store_true', default=False, + help='Treat value and thresholds as semantic versions.') + parser.add_argument('args', nargs=argparse.REMAINDER, help='Pairs of =. ' + 'For example 2=red 4=orange 6=yellow 8=good. ' + 'Read this as "Less than 2 = red, less than 4 = orange...".') + return parser.parse_args(args) + + +def main(args=None): + """Generate a badge based on command line arguments.""" + + # Args may be sent from command line of as args directly. + if not args: + args = sys.argv[1:] + + # Parse command line arguments + args = parse_args(args) + + label = args.label + threshold_text = args.args + suffix = args.suffix + + # Check whether thresholds were sent as one word, and is in the + # list of available styles. If so, swap in the style. + if len(args.args) == 1 and Style.exists(args.args[0].upper()): + style_name = args.args[0].upper() + style = Style[style_name] + threshold_text = style.threshold.split(' ') + if not args.label and style.label: + label = style.label + if not args.suffix and style.suffix: + suffix = style.suffix + + if not label: + raise ValueError('Label has not been set. Please use --label argument.') + + # Create threshold list from args + threshold_list = [x.split('=') for x in threshold_text] + threshold_dict = {x[0]: x[1] for x in threshold_list} + + # Create badge object + badge = Badge(label, args.value, value_prefix=args.prefix, value_suffix=suffix, + default_color=args.color, num_padding_chars=args.padding, + num_label_padding_chars=args.label_padding, num_value_padding_chars=args.value_padding, + font_name=args.font, font_size=args.font_size, template=args.template, style=args.style, + use_max_when_value_exceeds=args.use_max, thresholds=threshold_dict, + value_format=args.value_format, text_color=args.text_color, semver=args.semver) + + if args.file: + # Write badge SVG to file + badge.write_badge(args.file, overwrite=args.overwrite) + else: + print(badge.badge_svg_text) + + +if __name__ == '__main__': + main() diff --git a/anybadge/colors.py b/anybadge/colors.py new file mode 100644 index 0000000..4a5417c --- /dev/null +++ b/anybadge/colors.py @@ -0,0 +1,27 @@ +# Create a dictionary of colors to make selections +# easier. +from enum import Enum + + +class Color(Enum): + WHITE = '#FFFFFF' + SILVER = '#C0C0C0' + GRAY = '#808080' + BLACK = '#000000' + RED = '#e05d44' + BRIGHT_RED = '#FF0000' + MAROON = '#800000' + OLIVE = '#808000' + LIME = '#00FF00' + BRIGHT_YELLOW = '#FFFF00' + YELLOW = '#dfb317' + GREEN = '#4c1' + YELLOW_GREEN = '#a4a61d' + AQUA = '#00FFFF' + TEAL = '#008080' + BLUE = '#0000FF' + NAVY = '#000080' + FUCHSIA = '#FF00FF' + PURPLE = '#800080' + ORANGE = '#fe7d37' + LIGHT_GREY = '#9f9f9f' \ No newline at end of file diff --git a/anybadge/config.py b/anybadge/config.py new file mode 100644 index 0000000..c5d7efc --- /dev/null +++ b/anybadge/config.py @@ -0,0 +1,22 @@ +# Set some defaults +DEFAULT_FONT = 'DejaVu Sans,Verdana,Geneva,sans-serif' +DEFAULT_FONT_SIZE = 11 +NUM_PADDING_CHARS = 0.5 +DEFAULT_COLOR = '#4c1' +DEFAULT_TEXT_COLOR = '#fff' +MASK_ID_PREFIX = 'anybadge_' + +# Dictionary for looking up approx pixel widths of +# supported fonts and font sizes. +FONT_WIDTHS = { + 'DejaVu Sans,Verdana,Geneva,sans-serif': { + 10: 9, + 11: 10, + 12: 11, + }, + 'Arial, Helvetica, sans-serif': { + 11: 8, + }, +} + + diff --git a/anybadge/exceptions.py b/anybadge/exceptions.py new file mode 100644 index 0000000..693e974 --- /dev/null +++ b/anybadge/exceptions.py @@ -0,0 +1,3 @@ + +class UnknownBadgeTemplate(Exception): + """The badge template is unknown.""" diff --git a/anybadge/helpers.py b/anybadge/helpers.py new file mode 100644 index 0000000..6f7d360 --- /dev/null +++ b/anybadge/helpers.py @@ -0,0 +1,62 @@ +# Based on the following SO answer: https://stackoverflow.com/a/16008023/6252525 +def _get_approx_string_width(text, font_width, fixed_width=False): + """ + Get the approximate width of a string using a specific average font width. + + Args: + text(str): Text string to calculate width of. + font_width(int): Average width of font characters. + fixed_width(bool): Indicates that the font is fixed width. + + Returns: + int: Width of string in pixels. + + Examples: + + Call the function with a string and the maximum character width of the font you are using: + + >>> int(_get_approx_string_width('hello', 10)) + 29 + + This example shows the comparison of simplistic calculation based on a fixed width. + Given a test string and a fixed font width of 10, we can calculate the width + by multiplying the length and the font character with: + + >>> test_string = 'GOOGLE|ijkl' + >>> _get_approx_string_width(test_string, 10, fixed_width=True) + 110 + + Since some characters in the string are thinner than others we expect that the + apporximate text width will be narrower than the fixed width calculation: + + >>> _get_approx_string_width(test_string, 10) + 77 + + """ + if fixed_width: + return len(text) * font_width + + size = 0.0 + + # A dictionary containing percentages that relate to how wide + # each character will be represented in a variable width font. + # These percentages can be calculated using the ``_get_character_percentage_dict`` function. + char_width_percentages = { + "lij|' ": 40.0, + '![]fI.,:;/\\t': 50.0, + '`-(){}r"': 60.0, + '*^zcsJkvxy': 70.0, + 'aebdhnopqug#$L+<>=?_~FZT0123456789': 70.0, + 'BSPEAKVXY&UwNRCHD': 70.0, + 'QGOMm%W@': 100.0 + } + + for s in text: + percentage = 100.0 + for k in char_width_percentages.keys(): + if s in k: + percentage = char_width_percentages[k] + break + size += (percentage / 100.0) * float(font_width) + + return int(size) diff --git a/anybadge/server/cli.py b/anybadge/server/cli.py new file mode 100644 index 0000000..87e066b --- /dev/null +++ b/anybadge/server/cli.py @@ -0,0 +1,75 @@ +import argparse +import logging +from http.server import HTTPServer +from os import environ + +from anybadge.server.request_handler import AnyBadgeHTTPRequestHandler +from anybadge.server import config + +logger = logging.getLogger(__name__) + + +def run(listen_address: str = None, port: int = None): + if not listen_address: + listen_address = config.DEFAULT_SERVER_LISTEN_ADDRESS + + if not port: + port = config.DEFAULT_SERVER_PORT + + server_address = (listen_address, port) + + global SERVER_PORT, SERVER_LISTEN_ADDRESS + + SERVER_PORT = port + SERVER_LISTEN_ADDRESS = listen_address + + httpd = HTTPServer(server_address, AnyBadgeHTTPRequestHandler) + logger.info('Serving at: http://%s:%s' % server_address) + httpd.serve_forever() + + +def parse_args(): + logger.debug('Parsing command line arguments.') + parser = argparse.ArgumentParser(description="Run an anybadge server.") + parser.add_argument('-p', '--port', type=int, default=DEFAULT_SERVER_PORT, + help="Server port number. Default is %s. This can also be set via an environment " + "variable called ``ANYBADGE_PORT``." % DEFAULT_SERVER_PORT) + parser.add_argument('-l', '--listen-address', type=str, default=DEFAULT_SERVER_LISTEN_ADDRESS, + help="Server listen address. Default is %s. This can also be set via an environment " + "variable called ``ANYBADGE_LISTEN_ADDRESS``." % DEFAULT_SERVER_LISTEN_ADDRESS) + parser.add_argument('-d', '--debug', action='store_true', help='Enable debug logging.') + return parser.parse_args() + + +def main(): + """Run server.""" + + global DEFAULT_SERVER_PORT, DEFAULT_SERVER_LISTEN_ADDRESS, DEFAULT_LOGGING_LEVEL + + # Check for environment variables + if 'ANYBADGE_PORT' in environ: + DEFAULT_SERVER_PORT = environ['ANYBADGE_PORT'] + + if 'ANYBADGE_LISTEN_ADDRESS' in environ: + DEFAULT_SERVER_LISTEN_ADDRESS = environ['ANYBADGE_LISTEN_ADDRESS'] + + if 'ANYBADGE_LOG_LEVEL' in environ: + DEFAULT_LOGGING_LEVEL = logging.getLevelName(environ['ANYBADGE_LOG_LEVEL']) + + # Parse command line args + args = parse_args() + + # Set logging level + logging_level = DEFAULT_LOGGING_LEVEL + if args.debug: + logging_level = logging.DEBUG + + logging.basicConfig(format='%(asctime)-15s %(levelname)s:%(filename)s(%(lineno)d):%(funcName)s: %(message)s', + level=logging_level) + logger.info('Starting up anybadge server.') + + run(listen_address=args.listen_address, port=args.port) + + +if __name__ == '__main__': + main() diff --git a/anybadge/server/config.py b/anybadge/server/config.py new file mode 100644 index 0000000..ec88b55 --- /dev/null +++ b/anybadge/server/config.py @@ -0,0 +1,9 @@ +import logging + + +DEFAULT_SERVER_PORT = 8000 +DEFAULT_SERVER_LISTEN_ADDRESS = 'localhost' +DEFAULT_LOGGING_LEVEL = logging.INFO + +SERVER_PORT = DEFAULT_SERVER_PORT +SERVER_LISTEN_ADDRESS = DEFAULT_SERVER_LISTEN_ADDRESS diff --git a/anybadge/server/request_handler.py b/anybadge/server/request_handler.py new file mode 100644 index 0000000..ff73514 --- /dev/null +++ b/anybadge/server/request_handler.py @@ -0,0 +1,74 @@ +import logging +import urllib.parse as urlparse +from http.server import BaseHTTPRequestHandler + +from anybadge import Badge +from anybadge.server import config + +logger = logging.getLogger(__name__) + + +class AnyBadgeHTTPRequestHandler(BaseHTTPRequestHandler): + """Request handler for anybadge HTTP server.""" + + def do_HEAD(self): + logging.debug('Sending head.') + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + + def do_GET(self): + logging.debug('Handling get request.') + self.do_HEAD() + + # Ignore request for favicon + if self.path == '/favicon.ico': + logging.debug('Ignoring favicon request.') + return + + # Parse the URL query string + parsed = urlparse.urlparse(self.path) + url_query = urlparse.parse_qs(parsed.query) + + label = '' + value = '' + color = 'green' + + # Extract the label and value portions + if 'label' in url_query: + label = url_query['label'][0] + + if 'value' in url_query: + value = url_query['value'][0] + + logging.debug('Label: %s Value: %s', label, value) + + if label and value and color: + logging.debug('All parameters present.') + badge = Badge(label=label, value=value, default_color=color) + for line in badge.badge_svg_text: + self.wfile.write(str.encode(line)) + + else: + logging.debug('Not all parameters present.') + + self.wfile.write(b"Anybadge Web Server.") + self.wfile.write(b"") + + help_text = """ +

Welcome to the Anybadge Web Server.

+ + You are seeing this message because you haven't passed all the query parameters + to display a badge. + + You need to pass at least a label and value parameter. + + Here is an example: + + \ + http://localhost:{port}/?label=Project%20Awesomeness&value=110% + """.format(port=config.SERVER_PORT) + + for line in help_text.splitlines(): + self.wfile.write(str.encode('

%s

' % line)) + self.wfile.write(b"") \ No newline at end of file diff --git a/anybadge/styles.py b/anybadge/styles.py new file mode 100644 index 0000000..6a8befd --- /dev/null +++ b/anybadge/styles.py @@ -0,0 +1,25 @@ +from enum import Enum + + +# Define some templates that can be used for common badge types, saving +# from having to provide thresholds and labels each time. +class Style(Enum): + """A style that can be used for common badge types.""" + + PYLINT = ('default.svg', '2=red 4=orange 8=yellow 10=green', 'pylint') + COVERAGE = ('default.svg', '50=red 60=orange 80=yellow 100=green', 'coverage', '%') + + def __init__(self, template, threshold, label, suffix=None): + self.template = template + self.threshold = threshold + self.label = label + self.suffix = suffix + + @classmethod + def exists(cls, name: str): + """Test whether a style exists.""" + try: + _ = cls[name] + return True + except KeyError: + return False diff --git a/anybadge/templates/__init__.py b/anybadge/templates/__init__.py new file mode 100644 index 0000000..3872fdb --- /dev/null +++ b/anybadge/templates/__init__.py @@ -0,0 +1,19 @@ +"""Templates package.""" +import pkgutil + +from anybadge.exceptions import UnknownBadgeTemplate + + +def get_template(name: str) -> str: + """Get a template by name. + + Examples: + + >>> get_template('default') # doctest: +ELLIPSIS + ' + + + + + + + + + + + + + + + {{ label }} + {{ label }} + + + {{ value }} + {{ value }} + + \ No newline at end of file diff --git a/anybadge/templates/gitlab_scoped.svg b/anybadge/templates/gitlab_scoped.svg new file mode 100644 index 0000000..678292b --- /dev/null +++ b/anybadge/templates/gitlab_scoped.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + {{ label }} + + + {{ value }} + + \ No newline at end of file diff --git a/anybadge_server.py b/anybadge_server.py deleted file mode 100755 index 338bd67..0000000 --- a/anybadge_server.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/python -""" -Anybadge Server - -This package provides a server for anybadge. -""" -from __future__ import print_function -from os import environ -import logging -import argparse -from anybadge import Badge - -# Import the correct version of HTTP server -try: - from http.server import HTTPServer, BaseHTTPRequestHandler -except ImportError: - from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler - -# Import the correct version of urlparse, depending on which version -# of Python we are using -try: - import urlparse -except ImportError: - import urllib.parse as urlparse - - -logger = logging.getLogger(__name__) - -DEFAULT_SERVER_PORT = 8000 -DEFAULT_SERVER_LISTEN_ADDRESS = 'localhost' -DEFAULT_LOGGING_LEVEL = logging.INFO - -SERVER_PORT = DEFAULT_SERVER_PORT -SERVER_LISTEN_ADDRESS = DEFAULT_SERVER_LISTEN_ADDRESS - - -class AnybadgeHTTPRequestHandler(BaseHTTPRequestHandler): - """Request handler for Anybadge HTTP server.""" - - def do_HEAD(self): - logging.debug('Sending head.') - self.send_response(200) - self.send_header("Content-type", "text/html") - self.end_headers() - - def do_GET(self): - logging.debug('Handling get request.') - self.do_HEAD() - - # Ignore request for favicon - if self.path == '/favicon.ico': - logging.debug('Ignoring favicon request.') - return - - # Parse the URL query string - parsed = urlparse.urlparse(self.path) - url_query = urlparse.parse_qs(parsed.query) - - label = '' - value = '' - color = 'green' - - # Extract the label and value portions - if 'label' in url_query: - label = url_query['label'][0] - - if 'value' in url_query: - value = url_query['value'][0] - - logging.debug('Label: %s Value: %s', label, value) - - if label and value and color: - logging.debug('All parameters present.') - badge = Badge(label=label, value=value, default_color=color) - for line in badge.badge_svg_text: - self.wfile.write(str.encode(line)) - - else: - logging.debug('Not all parameters present.') - - self.wfile.write(b"Anybadge Web Server.") - self.wfile.write(b"") - - help_text = """ -

Welcome to the Anybadge Web Server.

- - You are seeing this message because you haven't passed all the query parameters - to display a badge. - - You need to pass at least a label and value parameter. - - Here is an example: - - \ - http://localhost:{port}/?label=Project%20Awesomeness&value=110% - """.format(port=SERVER_PORT) - - for line in help_text.splitlines(): - self.wfile.write(str.encode('

%s

' % line)) - self.wfile.write(b"") - - -def run(listen_address=DEFAULT_SERVER_LISTEN_ADDRESS, port=DEFAULT_SERVER_PORT): - server_address = (listen_address, port) - - global SERVER_PORT, SERVER_LISTEN_ADDRESS - - SERVER_PORT = port - SERVER_LISTEN_ADDRESS = listen_address - - httpd = HTTPServer(server_address, AnybadgeHTTPRequestHandler) - logging.info('Serving at: http://%s:%s' % server_address) - httpd.serve_forever() - - -def parse_args(): - logger.debug('Parsing command line arguments.') - parser = argparse.ArgumentParser(description="Run an anybadge server.") - parser.add_argument('-p', '--port', type=int, default=DEFAULT_SERVER_PORT, - help="Server port number. Default is %s. This can also be set via an environment " - "variable called ``ANYBADGE_PORT``." % DEFAULT_SERVER_PORT) - parser.add_argument('-l', '--listen-address', type=str, default=DEFAULT_SERVER_LISTEN_ADDRESS, - help="Server listen address. Default is %s. This can also be set via an environment " - "variable called ``ANYBADGE_LISTEN_ADDRESS``." % DEFAULT_SERVER_LISTEN_ADDRESS) - parser.add_argument('-d', '--debug', action='store_true', help='Enable debug logging.') - return parser.parse_args() - - -def main(): - """Run server.""" - - global DEFAULT_SERVER_PORT, DEFAULT_SERVER_LISTEN_ADDRESS, DEFAULT_LOGGING_LEVEL - - # Check for environment variables - if 'ANYBADGE_PORT' in environ: - DEFAULT_SERVER_PORT = environ['ANYBADGE_PORT'] - - if 'ANYBADGE_LISTEN_ADDRESS' in environ: - DEFAULT_SERVER_LISTEN_ADDRESS = environ['ANYBADGE_LISTEN_ADDRESS'] - - if 'ANYBADGE_LOG_LEVEL' in environ: - DEFAULT_LOGGING_LEVEL = logging.getLevelName(environ['ANYBADGE_LOG_LEVEL']) - - # Parse command line args - args = parse_args() - - # Set logging level - logging_level = DEFAULT_LOGGING_LEVEL - if args.debug: - logging_level = logging.DEBUG - - logging.basicConfig(format='%(asctime)-15s %(levelname)s:%(filename)s(%(lineno)d):%(funcName)s: %(message)s', - level=logging_level) - logger.info('Starting up anybadge server.') - - run(listen_address=args.listen_address, port=args.port) - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/examples/color_aqua.svg b/examples/color_aqua.svg index 8f94e9d..4df0408 100644 --- a/examples/color_aqua.svg +++ b/examples/color_aqua.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - aqua - aqua + Color.AQUA + Color.AQUA \ No newline at end of file diff --git a/examples/color_black.svg b/examples/color_black.svg index 50b40e1..5701302 100644 --- a/examples/color_black.svg +++ b/examples/color_black.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - black - black + Color.BLACK + Color.BLACK \ No newline at end of file diff --git a/examples/color_blue.svg b/examples/color_blue.svg index 2fb3b62..c427c7b 100644 --- a/examples/color_blue.svg +++ b/examples/color_blue.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - blue - blue + Color.BLUE + Color.BLUE \ No newline at end of file diff --git a/examples/color_fuchsia.svg b/examples/color_fuchsia.svg index 4e0d9c7..d1ea317 100644 --- a/examples/color_fuchsia.svg +++ b/examples/color_fuchsia.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - fuchsia - fuchsia + Color.FUCHSIA + Color.FUCHSIA \ No newline at end of file diff --git a/examples/color_gray.svg b/examples/color_gray.svg index e99fe36..a7f6ae7 100644 --- a/examples/color_gray.svg +++ b/examples/color_gray.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - gray - gray + Color.GRAY + Color.GRAY \ No newline at end of file diff --git a/examples/color_green.svg b/examples/color_green.svg index b99a269..21f6d9b 100644 --- a/examples/color_green.svg +++ b/examples/color_green.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - green - green + Color.GREEN + Color.GREEN \ No newline at end of file diff --git a/examples/color_lime.svg b/examples/color_lime.svg index 167150c..c61ee23 100644 --- a/examples/color_lime.svg +++ b/examples/color_lime.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - lime - lime + Color.LIME + Color.LIME \ No newline at end of file diff --git a/examples/color_maroon.svg b/examples/color_maroon.svg index cb96180..6e55e64 100644 --- a/examples/color_maroon.svg +++ b/examples/color_maroon.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - maroon - maroon + Color.MAROON + Color.MAROON \ No newline at end of file diff --git a/examples/color_navy.svg b/examples/color_navy.svg index 7a16fda..e832f43 100644 --- a/examples/color_navy.svg +++ b/examples/color_navy.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - navy - navy + Color.NAVY + Color.NAVY \ No newline at end of file diff --git a/examples/color_olive.svg b/examples/color_olive.svg index e0e0c64..2d33248 100644 --- a/examples/color_olive.svg +++ b/examples/color_olive.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - olive - olive + Color.OLIVE + Color.OLIVE \ No newline at end of file diff --git a/examples/color_orange.svg b/examples/color_orange.svg index fc43cf7..f8ad48b 100644 --- a/examples/color_orange.svg +++ b/examples/color_orange.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - orange - orange + Color.ORANGE + Color.ORANGE \ No newline at end of file diff --git a/examples/color_purple.svg b/examples/color_purple.svg index c02bb54..55b1373 100644 --- a/examples/color_purple.svg +++ b/examples/color_purple.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - purple - purple + Color.PURPLE + Color.PURPLE \ No newline at end of file diff --git a/examples/color_red.svg b/examples/color_red.svg index 570aa56..9532eb2 100644 --- a/examples/color_red.svg +++ b/examples/color_red.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - red - red + Color.RED + Color.RED \ No newline at end of file diff --git a/examples/color_silver.svg b/examples/color_silver.svg index bdffeea..a51adb4 100644 --- a/examples/color_silver.svg +++ b/examples/color_silver.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - silver - silver + Color.SILVER + Color.SILVER \ No newline at end of file diff --git a/examples/color_teal.svg b/examples/color_teal.svg index 6b81930..6a11828 100644 --- a/examples/color_teal.svg +++ b/examples/color_teal.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - teal - teal + Color.TEAL + Color.TEAL \ No newline at end of file diff --git a/examples/color_white.svg b/examples/color_white.svg index 3c64231..caca741 100644 --- a/examples/color_white.svg +++ b/examples/color_white.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - white - white + Color.WHITE + Color.WHITE \ No newline at end of file diff --git a/examples/color_yellow.svg b/examples/color_yellow.svg index a0a248a..f5225e0 100644 --- a/examples/color_yellow.svg +++ b/examples/color_yellow.svg @@ -1,23 +1,23 @@ - + - - + + - + - - + + Color Color - yellow - yellow + Color.YELLOW + Color.YELLOW \ No newline at end of file diff --git a/setup.py b/setup.py index a16d3d7..2becd57 100644 --- a/setup.py +++ b/setup.py @@ -20,18 +20,20 @@ setup( version=version, author='Jon Grace-Cox', author_email='jongracecox@gmail.com', - py_modules=['anybadge', 'anybadge_server'], + packages=['anybadge'], + py_modules=['anybadge_server'], setup_requires=['setuptools', 'wheel'], tests_require=[], install_requires=['packaging'], - data_files=[], + package_data={'anybadge': ['templates/*.svg']}, options={ - 'bdist_wheel': {'universal': True} + 'bdist_wheel': {'universal': False} }, + python_requires='>=3.4', url='https://github.com/jongracecox/anybadge', entry_points={ - 'console_scripts': ['anybadge=anybadge:main', - 'anybadge-server=anybadge_server:main'], + 'console_scripts': ['anybadge=anybadge.cli:main', + 'anybadge-server=anybadge.server.cli:main'], }, classifiers=[ 'License :: OSI Approved :: MIT License' diff --git a/tests/test_anybadge.py b/tests/test_anybadge.py index 9436971..fc6ed0a 100644 --- a/tests/test_anybadge.py +++ b/tests/test_anybadge.py @@ -1,5 +1,6 @@ from unittest import TestCase -from anybadge import Badge, parse_args, main +from anybadge import Badge +from anybadge.cli import main, parse_args class TestAnybadge(TestCase):