From 3a230018c469542ba64c8634906a4ddd8b71e044 Mon Sep 17 00:00:00 2001 From: Thomas Bui <43018778+Thomas-Boi@users.noreply.github.com> Date: Thu, 29 Apr 2021 01:04:32 -0700 Subject: [PATCH] Script upgrades and updated CONTRIBUTING.md and README.md (#576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated README and CONTRIBUTING * Added check for devicon object when peeking * Added PR template * Added a script to create release messages * Updated CONTRIBUTING about new script * Update .github/PULL_REQUEST_TEMPLATE/new_icon.md Co-authored-by: David Leal * Update .github/scripts/build_assets/arg_getters.py Co-authored-by: David Leal * Update .github/workflows/get_release_message.yml Co-authored-by: David Leal * Update gulpfile.js Co-authored-by: David Leal * Update .github/PULL_REQUEST_TEMPLATE/new_feature.md Co-authored-by: David Leal * Update .github/PULL_REQUEST_TEMPLATE/new_feature.md Co-authored-by: David Leal * Added a way for peek bot to comment error * Update CONTRIBUTING.md Co-authored-by: Clemens Bastian <8781699+amacado@users.noreply.github.com> * Update .github/scripts/get_release_message.py Co-authored-by: Malte Jürgens * Update .github/scripts/get_release_message.py Co-authored-by: Malte Jürgens * Update .github/PULL_REQUEST_TEMPLATE/new_feature.md Co-authored-by: David Leal * Clean up and updated CONTRIBUTING * Updated CONTRIBUTING * Add set up steps for release message workflow * Refactored peek workflow * Added requests library * Reformat devicon object error messages Co-authored-by: David Leal Co-authored-by: Clemens Bastian <8781699+amacado@users.noreply.github.com> Co-authored-by: Malte Jürgens --- .github/PULL_REQUEST_TEMPLATE/new_feature.md | 9 ++ .github/PULL_REQUEST_TEMPLATE/new_icon.md | 9 ++ .github/drafts/check_devicon_object.py | 37 ------- .github/scripts/build_assets/arg_getters.py | 13 ++- .github/scripts/get_release_message.py | 75 ++++++++++++++ .github/scripts/icomoon_peek.py | 103 ++++++++++++++----- .github/scripts/requirements.txt | 3 +- .github/workflows/get_release_message.yml | 23 +++++ .github/workflows/peek_icons.yml | 7 ++ .github/workflows/post_peek_screenshot.yml | 20 +++- CONTRIBUTING.md | 38 +++++-- README.md | 5 +- gulpfile.js | 1 + package.json | 5 +- 14 files changed, 266 insertions(+), 82 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE/new_feature.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/new_icon.md delete mode 100644 .github/drafts/check_devicon_object.py create mode 100644 .github/scripts/get_release_message.py create mode 100644 .github/workflows/get_release_message.yml diff --git a/.github/PULL_REQUEST_TEMPLATE/new_feature.md b/.github/PULL_REQUEST_TEMPLATE/new_feature.md new file mode 100644 index 000000000..2c605d599 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/new_feature.md @@ -0,0 +1,9 @@ +## This PR adds... + +*List your features here and their reasons for creation.* + +## Notes + +*List anything note-worthy here (potential issues, this needs merge to `master` before working, etc....).* + +*Don't forget to link any issues that this PR will solved.* diff --git a/.github/PULL_REQUEST_TEMPLATE/new_icon.md b/.github/PULL_REQUEST_TEMPLATE/new_icon.md new file mode 100644 index 000000000..cffee5f2a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/new_icon.md @@ -0,0 +1,9 @@ +**Double check these details before you open a PR** + +- [] PR does not match another non-stale PR currently opened +- [] PR name matches the format *new icon: Icon name (versions separated by comma)* as seen [here](https://github.com/devicons/devicon/blob/develop/CONTRIBUTING.md#overview) +- [] Your icons are put in a folder as seen [here](https://github.com/devicons/devicon/blob/develop/CONTRIBUTING.md#organizational-guidelines) +- [] SVG matches the standards laid out [here](https://github.com/devicons/devicon/blob/develop/CONTRIBUTING.md#svgStandards) +- [] A new object is added in the `devicon.json` file as seen [here](https://github.com/devicons/devicon/blob/develop/CONTRIBUTING.md#-updating-the-deviconjson-) + +Refer to the [`CONTRIBUTING.md`](https://github.com/devicons/devicon/blob/develop/CONTRIBUTING.md#contributing-to-devicon) for more details. diff --git a/.github/drafts/check_devicon_object.py b/.github/drafts/check_devicon_object.py deleted file mode 100644 index b610d88ed..000000000 --- a/.github/drafts/check_devicon_object.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import List - -# abandoned since it's not too hard to check devicon objects using our eyes -# however, still keep in case we need it in the future - -def check_devicon_objects(icons: List[dict]): - """ - Check that the devicon objects added is up to standard. - """ - err_msgs = [] - for icon in icons: - if type(icon["name"]) != str: - err_msgs.append("'name' must be a string, not: " + str(icon["name"])) - - try: - for tag in icon["tags"]: - if type(tag) != str: - raise TypeError() - except TypeError: - err_msgs.append("'tags' must be an array of strings, not: " + str(icon["tags"])) - break - - - if type(icon["versions"]["svg"]) != list or len(icon["versions"]["svg"]) == 0: - err_msgs.append("Icon name must be a string") - - if type(icon["versions"]["font"]) != list or len(icon["versions"]["svg"]) == 0: - err_msgs.append("Icon name must be a string") - - if type(icon["color"]) != str or "#" not in icon["color"]: - err_msgs.append("'color' must be a string in the format '#abcdef'") - - if type(icon["aliases"]) != list: - err_msgs.append("'aliases' must be an array of dicts") - - if len(err_msgs) > 0: - raise Exception("Error found in devicon.json: \n" + "\n".join(err_msgs)) diff --git a/.github/scripts/build_assets/arg_getters.py b/.github/scripts/build_assets/arg_getters.py index 51c5d557f..ebe37ea7d 100644 --- a/.github/scripts/build_assets/arg_getters.py +++ b/.github/scripts/build_assets/arg_getters.py @@ -67,4 +67,15 @@ def get_check_svgs_monthly_args(): parser.add_argument("icons_folder_path", help="The path to the icons folder", action=PathResolverAction) - return parser.parse_args() \ No newline at end of file + return parser.parse_args() + + +def get_release_message_args(): + """ + Get the commandline arguments for get_release_message.py. + """ + parser = ArgumentParser(description="Create a text containing the icons and features added since last release.") + parser.add_argument("token", + help="The GitHub token to access the GitHub REST API.", + type=str) + return parser.parse_args() diff --git a/.github/scripts/get_release_message.py b/.github/scripts/get_release_message.py new file mode 100644 index 000000000..af8b6b78c --- /dev/null +++ b/.github/scripts/get_release_message.py @@ -0,0 +1,75 @@ +import requests +from build_assets import arg_getters +import re + +def main(): + print("Please wait a few seconds...") + args = arg_getters.get_release_message_args() + queryPath = "https://api.github.com/repos/devicons/devicon/pulls?accept=application/vnd.github.v3+json&state=closed&per_page=100" + stopPattern = r"^(r|R)elease v" + headers = { + "Authorization": f"token {args.token}" + } + + response = requests.get(queryPath, headers=headers) + if not response: + print(f"Can't query the GitHub API. Status code is {response.status_code}. Message is {response.text}") + return + + data = response.json() + newIcons = [] + features = [] + + for pullData in data: + if re.search(stopPattern, pullData["title"]): + break + + authors = findAllAuthors(pullData, headers) + markdown = f"- [{pullData['title']}]({pullData['html_url']}) by {authors}." + + if isFeatureIcon(pullData): + newIcons.append(markdown) + else: + features.append(markdown) + + thankYou = "A huge thanks to all our maintainers and contributors for making this release possible!" + iconTitle = "**{} New Icons**\n".format(len(newIcons)) + featureTitle = "**{} New Features**\n".format(len(features)) + finalString = "{0}\n\n {1}{2}\n\n {3}{4}".format(thankYou, + iconTitle, "\n".join(newIcons), featureTitle, "\n".join(features)) + + print("--------------Here is the build message--------------\n", finalString) + + +""" + Check whether the pullData is a feature:icon PR. + :param pullData + :return true if the pullData has a label named "feature:icon" +""" +def isFeatureIcon(pullData): + for label in pullData["labels"]: + if label["name"] == "feature:icon": + return True + return False + + +""" +Find all the authors of a PR based on its commits. +:param pullData - the data of a pull request. +""" +def findAllAuthors(pullData, authHeader): + response = requests.get(pullData["commits_url"], headers=authHeader) + if not response: + print(f"Can't query the GitHub API. Status code is {response.status_code}") + print("Response is: ", response.text) + return + + commits = response.json() + authors = set() # want unique authors only + for commit in commits: + authors.add("@" + commit["author"]["login"]) + return ", ".join(list(authors)) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/icomoon_peek.py b/.github/scripts/icomoon_peek.py index b4c993cb0..9e242f10a 100644 --- a/.github/scripts/icomoon_peek.py +++ b/.github/scripts/icomoon_peek.py @@ -2,7 +2,6 @@ import re import sys from selenium.common.exceptions import TimeoutException -import xml.etree.ElementTree as et # pycharm complains that build_assets is an unresolved ref # don't worry about it, the script still runs @@ -12,36 +11,28 @@ def main(): - args = arg_getters.get_selenium_runner_args(True) - new_icons = filehandler.find_new_icons(args.devicon_json_path, args.icomoon_json_path) - - # get only the icon object that has the name matching the pr title - filtered_icons = find_object_added_in_this_pr(new_icons, args.pr_title) - - if len(new_icons) == 0: - sys.exit("No files need to be uploaded. Ending script...") + runner = None + try: + args = arg_getters.get_selenium_runner_args(True) + new_icons = filehandler.find_new_icons(args.devicon_json_path, args.icomoon_json_path) - if len(filtered_icons) == 0: - message = "No icons found matching the icon name in the PR's title.\n" \ - "Ensure that a new icon entry is added in the devicon.json and the PR title matches the convention here: \n" \ - "https://github.com/devicons/devicon/blob/master/CONTRIBUTING.md#overview\n" \ - "Ending script...\n" - sys.exit(message) + if len(new_icons) == 0: + raise Exception("No files need to be uploaded. Ending script...") - # print list of new icons - print("List of new icons:", *new_icons, sep = "\n") - print("Icons being uploaded:", *filtered_icons, sep = "\n", end='\n\n') + # get only the icon object that has the name matching the pr title + filtered_icon = find_object_added_in_this_pr(new_icons, args.pr_title) + print("Icon being checked:", filtered_icon, sep = "\n", end='\n\n') - runner = None - try: runner = SeleniumRunner(args.download_path, args.geckodriver_path, args.headless) - svgs = filehandler.get_svgs_paths(filtered_icons, args.icons_folder_path, True) + svgs = filehandler.get_svgs_paths([filtered_icon], args.icons_folder_path, True) screenshot_folder = filehandler.create_screenshot_folder("./") runner.upload_svgs(svgs, screenshot_folder) print("Task completed.") - except TimeoutException as e: - util.exit_with_err("Selenium Time Out Error: \n" + str(e)) + + # no errors, do this so upload-artifact won't fail + filehandler.write_to_file("./err_messages.txt", "0") except Exception as e: + filehandler.write_to_file("./err_messages.txt", str(e)) util.exit_with_err(e) finally: runner.close() @@ -52,19 +43,77 @@ def find_object_added_in_this_pr(icons: List[dict], pr_title: str): Find the icon name from the PR title. :param icons, a list of the font objects found in the devicon.json. :pr_title, the title of the PR that this workflow was called on. - :return a list containing the dictionary with the "name" + :return a dictionary with the "name" entry's value matching the name in the pr_title. - If none can be found, return an empty list. + :raise If no object can be found, raise an Exception. """ try: pattern = re.compile(r"(?<=^new icon: )\w+ (?=\(.+\))", re.I) icon_name = pattern.findall(pr_title)[0].lower().strip() # should only have one match - return [icon for icon in icons if icon["name"] == icon_name] + icon = [icon for icon in icons if icon["name"] == icon_name][0] + check_devicon_object(icon, icon_name) + return icon except IndexError: # there are no match in the findall() - return [] + raise Exception("Couldn't find an icon matching the name in the PR title.") + except ValueError as e: + raise Exception(str(e)) + + +def check_devicon_object(icon: dict, icon_name: str): + """ + Check that the devicon object added is up to standard. + :return a string containing the error messages if any. + """ + err_msgs = [] + try: + if icon["name"] != icon_name: + err_msgs.append("- 'name' value is not: " + icon_name) + except KeyError: + err_msgs.append("- missing key: 'name'.") + + try: + for tag in icon["tags"]: + if type(tag) != str: + raise TypeError() + except TypeError: + err_msgs.append("- 'tags' must be an array of strings, not: " + str(icon["tags"])) + except KeyError: + err_msgs.append("- missing key: 'tags'.") + + try: + if type(icon["versions"]) != dict: + err_msgs.append("- 'versions' must be an object.") + except KeyError: + err_msgs.append("- missing key: 'versions'.") + try: + if type(icon["versions"]["svg"]) != list or len(icon["versions"]["svg"]) == 0: + err_msgs.append("- must contain at least 1 svg version in a list.") + except KeyError: + err_msgs.append("- missing key: 'svg' in 'versions'.") + + try: + if type(icon["versions"]["font"]) != list or len(icon["versions"]["svg"]) == 0: + err_msgs.append("- must contain at least 1 font version in a list.") + except KeyError: + err_msgs.append("- missing key: 'font' in 'versions'.") + try: + if type(icon["color"]) != str or "#" not in icon["color"]: + err_msgs.append("- 'color' must be a string in the format '#abcdef'") + except KeyError: + err_msgs.append("- missing key: 'color'.") + try: + if type(icon["aliases"]) != list: + err_msgs.append("- 'aliases' must be an array.") + except KeyError: + err_msgs.append("- missing key: 'aliases'.") + + if len(err_msgs) > 0: + message = "Error found in 'devicon.json' for '{}' entry: \n{}".format(icon_name, "\n".join(err_msgs)) + raise ValueError(message) + return "" if __name__ == "__main__": main() diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 27bc3be5d..00691033d 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1 +1,2 @@ -selenium==3.141.0 \ No newline at end of file +selenium==3.141.0 +requests==2.25.1 \ No newline at end of file diff --git a/.github/workflows/get_release_message.yml b/.github/workflows/get_release_message.yml new file mode 100644 index 000000000..292525e93 --- /dev/null +++ b/.github/workflows/get_release_message.yml @@ -0,0 +1,23 @@ +name: Get Release Message +on: workflow_dispatch +jobs: + build: + name: Get Fonts From Icomoon + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + + - name: Setup Python v3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r ./.github/scripts/requirements.txt + + - name: Run the get_release_message.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: python ./.github/scripts/get_release_message.py $GITHUB_TOKEN diff --git a/.github/workflows/peek_icons.yml b/.github/workflows/peek_icons.yml index 20f3455f1..c85d7393b 100644 --- a/.github/workflows/peek_icons.yml +++ b/.github/workflows/peek_icons.yml @@ -44,6 +44,13 @@ jobs: ./.github/scripts/build_assets/geckodriver-v0.27.0-win64/geckodriver.exe ./icomoon.json ./devicon.json ./icons ./ --headless --pr_title "%PR_TITLE%" + - name: Upload the err messages (created by icomoon_peek.py) + uses: actions/upload-artifact@v2 + if: always() + with: + name: err_messages + path: ./err_messages.txt + - name: Upload screenshots for comments uses: actions/upload-artifact@v2 if: success() diff --git a/.github/workflows/post_peek_screenshot.yml b/.github/workflows/post_peek_screenshot.yml index 15ae7ea71..59b55e4a4 100644 --- a/.github/workflows/post_peek_screenshot.yml +++ b/.github/workflows/post_peek_screenshot.yml @@ -32,6 +32,14 @@ jobs: with: path: ./pr_num/pr_num.txt + - name: Read the err message file + if: success() + id: err_message_reader + uses: juliangruber/read-file-action@v1.0.0 + with: + path: ./err_messages/err_messages.txt + + - name: Upload screenshot of the newly made icons gotten from the artifacts id: icons_overview_img_step if: env.PEEK_STATUS == 'success' && success() @@ -87,15 +95,19 @@ jobs: MESSAGE: | Hi there, - I'm Devicons' Peek Bot and it seems we've ran into a problem (sorry!). + I'm Devicons' Peek Bot and it seems we've ran into a problem. + + ``` + {0} + ``` - Please double check and fix the possible issues below: + Make sure that: - Your svgs are named and added correctly to the /icons folder as seen [here](https://github.com/devicons/devicon/blob/master/CONTRIBUTING.md#orgGuidelines). - Your icon information has been added to the `devicon.json` as seen [here](https://github.com/devicons/devicon/blob/master/CONTRIBUTING.md#updateDevicon) - Your PR title follows the format seen [here](https://github.com/devicons/devicon/blob/master/CONTRIBUTING.md#overview) - I will retry once everything is fixed. If I still fail (sorry!) or there are other erros, the maintainers will investigate. + I will retry once everything is fixed. If I still fail or there are other error, the maintainers will investigate. Best of luck, Peek Bot :relaxed: @@ -103,4 +115,4 @@ jobs: type: create issue_number: ${{ steps.pr_num_reader.outputs.content }} token: ${{ secrets.GITHUB_TOKEN }} - body: ${{ env.MESSAGE }} + body: ${{ format(env.MESSAGE, steps.err_message_reader.outputs.content) }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f825964a..7820950c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,6 +40,7 @@ First of all, thanks for taking the time to contribute! This project can only gr
  • Include the name of the Icon in the pull request title in this format: new icon: Icon name (versions)
  • Optional: Add images of the new svg(s) to the description of the pull request. This would help speed up the review process
  • Optional: Reference the issues regarding the new icon.
  • +
  • A bot will check your SVGs. If there are any errors, please fix them as instructed.
  • Wait for a maintainer to review your changes. They will run a script to check your icons.
  • If there are no issues, they will accept your pull request and merge it using squash merging. If there are any problems, they will let you know and give you a chance to fix it.
  • @@ -91,7 +92,6 @@ First of all, thanks for taking the time to contribute! This project can only gr
  • Each .svg file contains one version of an icon in a 0 0 128 128 viewbox. You can use a service like resize-image for scaling the svg.
  • The svg element does not need the height and width attributes. However, if you do use it, ensure their values are either "128" or "128px". Ex: height="128"
  • Each .svg must use the fill attribute instead of using classes for colors. See here for more details.
  • -
  • The naming convention for the svg file is the following: (Technology name)-(original|plain|line)(-wordmark?).

  • @@ -310,19 +310,20 @@ As an example, let's assume you have created the svgs for Redhat and Amazon Web

    The Build Script: how it works and its quirks

    -

    To make adding icons easier for repo maintainers, we rely on GitHub Actions, Python, Selenium, and Gulp to automate our tasks.

    +

    We rely on GitHub Actions, Python, Selenium, Imgur, and Gulp to automate our tasks. Please feel free to take a look at the workflow files. The codes should be clear enough to follow along.

    So far, the tasks in the build script are:

    -

    There are some quirks and bugs that the build scripts might run into. Listed below are the common ones and their solution

    +

    There are some bugs that the build scripts might run into. Listed below are the common ones and their solutions

    1. No connection could be made because the target machine actively refused it. (os error 10061)
        @@ -351,6 +352,13 @@ As an example, let's assume you have created the svgs for Redhat and Amazon Web
      • Solution: Follow the steps laid out here
    2. +
    3. Icon created by Icomoon contains strange lines that aren't in the SVG +
        +
      • See this PR's peek result.
      • +
      • This is caused by a bug in Icomoon's parser (see this).
      • +
      • Solution: Luckily this is an extremely rare case. Try remake the svg in a different way (using different paths/shapes) and test using Icomoon.
      • +
      +

    Discord server

    @@ -379,7 +387,19 @@ We are running a Discord server. You can go here to talk, discuss, and more with
  • Push the branch draft-release
  • Manually trigger the workflow build_icons.yml (which has a workflow_dispatch event trigger) and select the branch draft-release as target branch. This will build a font version of all icons using icomoon and automatically creates a pull request to merge the build result back into draft-release
  • Review and approve the auto-create pull request created by the action of the step above
  • -
  • Create a pull request towards development. Mention the release number in the pull request title (like "Build preparation for release vMAJOR.MINOR.PATCH) and add information about all new icons, fixes, features and enhancements in the description of the pull request. Take the commits as a guideline. It's also a good idea to mention and thank all contributions who participated in the release (take description of #504 as an example).
  • +
  • Create a pull request towards development. Mention the release number in the pull request title (like "Build preparation for release vMAJOR.MINOR.PATCH). +
      +
    • + Add information about all new icons, fixes, features and enhancements in the description of the pull request. +
    • +
    • + Take the PRs/commits as a guideline. It's also a good idea to mention and thank all contributions who participated in the release (take description of #504 as an example). +
    • +
    • + Rather than doing it manually, you can instead run python ./.github/scripts/get_release_message.py $GITHUB_TOKEN locally. Pass in your GitHub Personal Access Token for $GITHUB_TOKEN and you should see the messages. You can also use the `workflow_dispatch` trigger in the GitHub Actions tab. +
    • +
    +
  • Wait for review and approval of the pull request (you can perform a squash-merge)
  • Once merged create a pull request with BASE master and HEAD development. Copy the description of the earlier pull request.
  • Since it was already approved in the 'development' stage a maintainer is allowed to merge it (DON'T perform a squash-merge).
  • diff --git a/README.md b/README.md index c0e6bae1a..5b08278d5 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,11 @@ All product names, logos, and brands are property of their respective owners. All company, product and service names used in this website are for identification purposes only. Use of these names, logos, and brands does not - imply endorsement. + imply endorsement. Usage of these logos should be done according to the company/brand/service's brand policy. +

    + Thank you to our contributors and the IcoMoon app. Devicon would not be possible without you. +

    Getting started

    diff --git a/gulpfile.js b/gulpfile.js index 1541ffc10..24e29721e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -10,6 +10,7 @@ const aliasSCSSName = "devicon-alias.scss"; const colorsCSSName = "devicon-colors.css"; const finalMinSCSSName = "devicon.min.scss"; +//////// CSS Tasks //////// /** * Create the devicon.min.css by creating needed diff --git a/package.json b/package.json index dc43b4882..c8e473637 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "build-css": "gulp updateCss && gulp clean", "peek-test": "python ./.github/scripts/icomoon_peek.py ./.github/scripts/build_assets/geckodriver-v0.27.0-win64/geckodriver.exe ./icomoon.json ./devicon.json ./icons ./ --pr_title \"%PR_TITLE%\"", - "build-test": "python ./.github/scripts/icomoon_build.py ./.github/scripts/build_assets/geckodriver-v0.27.0-win64/geckodriver.exe ./icomoon.json ./devicon.json ./icons ./" + "build-test": "python ./.github/scripts/icomoon_build.py ./.github/scripts/build_assets/geckodriver-v0.27.0-win64/geckodriver.exe ./icomoon.json ./devicon.json ./icons ./", }, "repository": { "type": "git", @@ -28,5 +28,6 @@ "gulp": "^4.0.0", "gulp-sass": "^4.1.0", "sass": "^1.26.10" - } + }, + "dependencies": {} }