diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index a5bb6de..2d0e85f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,6 +1,6 @@ -blank_issues_enabled: false +blank_issues_enabled: true contact_links: - name: Ask a question url: https://github.com/hcookie/contributors/discussions/new - about: Ask a question or start a discussion \ No newline at end of file + about: Ask a question or start a discussion diff --git a/.github/workflows/contributors_report.yaml b/.github/workflows/contributors_report.yaml index e77cd36..a36d65b 100644 --- a/.github/workflows/contributors_report.yaml +++ b/.github/workflows/contributors_report.yaml @@ -4,7 +4,9 @@ on: workflow_dispatch: schedule: - cron: "3 2 1 * *" - +# on: +# push: +# branches: [testing] permissions: contents: read @@ -30,18 +32,22 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@1286dc8d6904a9a7f735e28b7503be164fb7d4b9 + uses: hcookie/organizational_contributors@1286dc8d6904a9a7f735e28b7503be164fb7d4b9 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} END_DATE: ${{ env.END_DATE }} - REPOSITORY: github/contributors - SPONSOR_INFO: "true" + REPOSITORY: hcookie/organizational_contributors + # SPONSOR_INFO: "true" + + - name: Show Contributor + shell: bash + run: cat contributors.md >> $GITHUB_STEP_SUMMARY - - name: Create issue - uses: peter-evans/create-issue-from-file@e8ef132d6df98ed982188e460ebb3b5d4ef3a9cd - with: - title: Monthly contributor report - token: ${{ secrets.GITHUB_TOKEN }} - content-filepath: ./contributors.md - assignees: zkoppert + # - name: Create issue + # uses: peter-evans/create-issue-from-file@e8ef132d6df98ed982188e460ebb3b5d4ef3a9cd + # with: + # title: Monthly contributor report + # token: ${{ secrets.GITHUB_TOKEN }} + # content-filepath: ./contributors.md + # # assignees: zkoppert diff --git a/.github/workflows/contributors_report_testing.yaml b/.github/workflows/contributors_report_testing.yaml new file mode 100644 index 0000000..0c9cf26 --- /dev/null +++ b/.github/workflows/contributors_report_testing.yaml @@ -0,0 +1,72 @@ +--- +name: Testing contributor report +# on: +# workflow_dispatch: +# schedule: +# - cron: "3 2 1 * *" +on: + push: + branches: [testing] +permissions: + contents: read + +jobs: + contributor_report: + name: contributor report + runs-on: ubuntu-latest + permissions: + issues: write + + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + + - name: Get dates for last month + shell: bash + run: | + # Calculate the first day of the previous month + start_date=$(date -d "last month" +%Y-%m-01) + + # Calculate the last day of the previous month + end_date=$(date -d "$start_date +1 month -1 day" +%Y-%m-%d) + + #Set an environment variable with the date range + # echo "START_DATE=$start_date" >> "$GITHUB_ENV" + # echo "END_DATE=$end_date" >> "$GITHUB_ENV" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run Contributors + run: python -m contributors + + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # START_DATE: ${{ env.START_DATE }} + # END_DATE: ${{ env.END_DATE }} + REPOSITORY: hcookie/organizational_contributors + SHOW_ORGANIZATIONS: ecmwf + # SPONSOR_INFO: "true" + + - name: Run Contributors with multiple repos + run: python -m contributors + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # START_DATE: ${{ env.START_DATE }} + # END_DATE: ${{ env.END_DATE }} + REPOSITORY: hcookie/organizational_contributors,ecmwf/anemoi-training + SHOW_ORGANIZATIONS: ecmwf, ecmwf-lab + # SPONSOR_INFO: "true" + + - name: Show Contributor + shell: bash + run: cat contributors.md >> $GITHUB_STEP_SUMMARY + + # - name: Create issue + # uses: peter-evans/create-issue-from-file@e8ef132d6df98ed982188e460ebb3b5d4ef3a9cd + # with: + # title: Monthly contributor report + # token: ${{ secrets.GITHUB_TOKEN }} + # content-filepath: ./contributors.md + # # assignees: zkoppert diff --git a/.github/workflows/super-linter.yaml b/.github/workflows/super-linter.yaml deleted file mode 100644 index 20dd739..0000000 --- a/.github/workflows/super-linter.yaml +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: Lint Code Base - -on: - pull_request: - branches: [main] - -permissions: - contents: read - -jobs: - build: - name: Lint Code Base - runs-on: ubuntu-latest - permissions: - contents: read - packages: read - statuses: write - steps: - - name: Checkout Code - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - with: - fetch-depth: 0 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt -r requirements-test.txt - - name: Lint Code Base - uses: super-linter/super-linter@b92721f792f381cedc002ecdbb9847a15ece5bb8 - env: - DEFAULT_BRANCH: main - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_ACTIONS_COMMAND_ARGS: -shellcheck= diff --git a/LICENSE b/LICENSE index 545898a..f49a4e1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,201 @@ -MIT License - -Copyright (c) 2023 GitHub - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/Makefile b/Makefile index 69a9f60..7aeae36 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,6 @@ lint: # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --config=.github/linters/.flake8 --count --exit-zero --max-complexity=15 --max-line-length=150 isort --settings-file=.github/linters/.isort.cfg . - pylint --rcfile=.github/linters/.python-lint --fail-under=9.0 *.py - mypy --config-file=.github/linters/.mypy.ini *.py - black . + pylint --rcfile=.github/linters/.python-lint --fail-under=9.0 contributors/*.py + mypy --config-file=.github/linters/.mypy.ini contributors/*.py + black . --line-length 120 diff --git a/README.md b/README.md index 70319ca..7c13885 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,26 @@ -# Contributors sorted by Organisation action +# Contributors sorted by Organization action [![Python package](https://github.com/hcookie/contributors/actions/workflows/python-ci.yml/badge.svg)](https://github.com/hcookie/contributors/actions/workflows/python-ci.yml) [![Docker Image CI](https://github.com/hcookie/contributors/actions/workflows/docker-ci.yml/badge.svg)](https://github.com/hcookie/contributors/actions/workflows/docker-ci.yml) [![CodeQL](https://github.com/hcookie/contributors/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/hcookie/contributors/actions/workflows/github-code-scanning/codeql)[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/github/contributors/badge)](https://scorecard.dev/viewer/?uri=github.com/github/contributors) -This is a GitHub Action that given an organization or specified repositories, produces information about the [contributors](https://chaoss.community/kb/metric-contributors/) sorted by organisation. +This is a GitHub Action that given an organization or specified repositories, produces information about the [contributors](https://chaoss.community/kb/metric-contributors/) sorted by organization. Similar actions to help you recognize contributors by putting them into a `README` or `CONTRIBUTORS.md` include: - [contributor-list](https://github.com/marketplace/actions/contribute-list) - [contributors](https://github.com/github/contributors) -## Example use cases +If no organisations to show are given, this action is effectively the same as [contributors](https://github.com/github/contributors). -- As a maintainer, you may want to acknowledge contributors from various organisations in a discussion post +## Example use cases +- As a maintainer, you may want to acknowledge contributors from various organizations in a discussion post +- A repository wants to track contributions from organizations ## Support -If you need support using this project or have questions about it, please [open up an issue in this repository](https://github.com/github/contributors/issues). +If you need support using this project or have questions about it, please [open up an issue in this repository](https://github.com/hcookie/organizational_contributors/issues). ## What is a contributor? @@ -80,8 +82,8 @@ This action can be configured to authenticate with GitHub App Installation or Pe | `END_DATE` | False | Current Date | The date at which you want to stop gathering contributor information. Must be later than the `START_DATE`. ie. Aug 2nd, 2023 would be `2023-08-02` | | `SPONSOR_INFO` | False | False | If you want to include sponsor information in the output. This will include the sponsor count and the sponsor URL. This will impact action performance. ie. SPONSOR_INFO = "False" or SPONSOR_INFO = "True" | | `LINK_TO_PROFILE` | False | True | If you want to link usernames to their GitHub profiles in the output. ie. LINK_TO_PROFILE = "True" or LINK_TO_PROFILE = "False" | - -**Note**: If `start_date` and `end_date` are specified then the action will determine if the contributor is new. A new contributor is one that has contributed in the date range specified but not before the start date. +| `SHOW_ORGANIZATIONS` | False | [] | Organizations to show in the contributors table. Will be evaluated in order, and a user only added to only the first one they are a part of. Any contributors with no organization will be shown in independent. Set to 'all' to show all organizations. +| `CONTRIB_FILENAME` | False | "contibutors" | Filename to add contributors to. Will create both an 'md', and 'json' file with contents. **Performance Note:** Using start and end dates will reduce speed of the action by approximately 63X. ie without dates if the action takes 1.7 seconds, it will take 1 minute and 47 seconds. @@ -121,13 +123,18 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@v1 + uses: hcookie/organizational_contributors@v1 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} END_DATE: ${{ env.END_DATE }} ORGANIZATION: - SPONSOR_INFO: "true" + SHOW_ORGANIZATIONS: ORGANIZATIONS_TO_SHOW_HERE,ANOTHER_ONE_HERE + CONTRIB_FILENAME: "contributors" + + - name: Show Contributor + shell: bash + run: cat contributors.md >> $GITHUB_STEP_SUMMARY - name: Create issue uses: peter-evans/create-issue-from-file@v5 @@ -144,31 +151,17 @@ jobs: # Contributors - Date range for contributor list: 2021-01-01 to 2023-10-10 -- Organization: super-linter +- Organization: ORGANIZATION_HERE | Total Contributors | Total Contributions | % new contributors | | ------------------ | ------------------- | ------------------ | | 1 | 143 | 0% | +## ORGANIZATION_HERE + | Username | All Time Contribution Count | New Contributor | Commits between 2021-01-01 and 2023-10-10 | | --------- | --------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| @zkoppert | 143 | False | [super-linter/super-linter](https://github.com/super-linter/super-linter/commits?author=zkoppert&since=2021-01-01&until=2023-10-10) | -``` - -## Example Markdown output with no dates supplied - -```markdown -#### Contributors - -- Organization: super-linter - -| Total Contributors | Total Contributions | % new contributors | -| ------------------ | ------------------- | ------------------ | -| 1 | 1913 | 0% | - -| Username | All Time Contribution Count | New Contributor | Sponsor URL | Commits between 2021-09-01 and 2023-09-30 | -| --------- | --------------------------- | --------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| @zkoppert | 1913 | False | [Sponsor Link](https://github.com/sponsors/zkoppert) | [super-linter/super-linter](https://github.com/super-linter/super-linter/commits?author=zkoppert&since=2021-09-01&until=2023-09-30) | +| @hcookie | 143 | False | [organization/repo]() | ``` ## Local usage without Docker @@ -178,12 +171,8 @@ jobs: 1. Fill out the `.env` file with a _token_ from a user that has access to the organization to scan (listed below). Tokens should have at least read:org access for organization scanning and read:repository for repository scanning. 1. Fill out the `.env` file with the configuration parameters you want to use 1. `pip3 install -r requirements.txt` -1. Run `python3 ./contributors.py`, which will output everything in the terminal +1. Run `python3 -m contributors`, which will output everything in the terminal ## License -[MIT](LICENSE) - -## More OSPO Tools - -Looking for more resources for your open source program office (OSPO)? Check out the [`github-ospo`](https://github.com/github/github-ospo) repo for a variety of tools designed to support your needs. +[Apache 2](LICENSE) diff --git a/action.yml b/action.yml index b05a592..524b042 100644 --- a/action.yml +++ b/action.yml @@ -1,10 +1,10 @@ --- -name: "Contributors Action" -author: "github" -description: "A GitHub Action to report out contributors and contributions to a repository or organization" +name: "Contributors by Organisation Action" +author: "hcookie" +description: "A GitHub Action to report out contributors and contributions to a repository or organization sorted by organisations" runs: using: "docker" - image: "docker://ghcr.io/github/contributors:v1" + image: "docker://ghcr.io/hcookie/organizational_contributors:v1" branding: icon: "users" color: "green" diff --git a/contributors.py b/contributors/__init__.py similarity index 64% rename from contributors.py rename to contributors/__init__.py index 6518fbc..75b7508 100644 --- a/contributors.py +++ b/contributors/__init__.py @@ -1,56 +1,48 @@ -# pylint: disable=broad-exception-caught -"""This file contains the main() and other functions needed to get contributor information from the organization or repository""" +"""Contributors Module""" +# pylint: disable=broad-exception-caught from typing import List -import auth -import contributor_stats -import env -import json_writer -import markdown +import github3 +import github3.repos +import github3.users + +from . import auth, contributor_stats, env, json_writer, markdown def main(): """Run the main program""" # Get environment variables - ( - organization, - repository_list, - gh_app_id, - gh_app_installation_id, - gh_app_private_key_bytes, - token, - ghe, - start_date, - end_date, - sponsor_info, - link_to_profile, - ) = env.get_env_vars() + environment = env.get_env_vars() # Auth to GitHub.com - github_connection = auth.auth_to_github( - gh_app_id, gh_app_installation_id, gh_app_private_key_bytes, token, ghe + github_connection: github3.GitHub = auth.auth_to_github( + environment.gh_app_id, + environment.gh_app_installation_id, + environment.gh_app_private_key_bytes, + environment.token, + environment.ghe, ) # Get the contributors contributors = get_all_contributors( - organization, - repository_list, - start_date, - end_date, + environment.organization, + environment.repositories_list, + environment.start_date, + environment.end_date, github_connection, ) # Check for new contributor if user provided start_date and end_date - if start_date and end_date: + if environment.start_date and environment.end_date: # get the list of contributors from before start_date # so we can see if contributors after start_date are new or returning returning_contributors = get_all_contributors( - organization, - repository_list, + environment.organization, + environment.repositories_list, start_date="2008-02-29", # GitHub was founded on 2008-02-29 - end_date=start_date, + end_date=environment.start_date, github_connection=github_connection, ) for contributor in contributors: @@ -59,29 +51,31 @@ def main(): ) # Get sponsor information on the contributor - if sponsor_info == "true": - contributors = contributor_stats.get_sponsor_information(contributors, token) + if environment.sponsor_info == "true": + contributors = contributor_stats.get_sponsor_information(contributors, environment.token) # Output the contributors information - # print(contributors) markdown.write_to_markdown( contributors, - "contributors.md", - start_date, - end_date, - organization, - repository_list, - sponsor_info, - link_to_profile, + f"{environment.filename}.md", + environment.start_date, + environment.end_date, + environment.organization, + environment.repositories_list, + environment.sponsor_info, + environment.link_to_profile, + environment.show_organizations_list, ) + json_writer.write_to_json( - filename="contributors.json", - start_date=start_date, - end_date=end_date, - organization=organization, - repository_list=repository_list, - sponsor_info=sponsor_info, - link_to_profile=link_to_profile, + filename="{environment.filename}.json", + start_date=environment.start_date, + end_date=environment.end_date, + organization=environment.organization, + repository_list=environment.repositories_list, + sponsor_info=environment.sponsor_info, + link_to_profile=environment.link_to_profile, contributors=contributors, + show_organizations_list=environment.show_organizations_list, ) @@ -90,7 +84,7 @@ def get_all_contributors( repository_list: List[str], start_date: str, end_date: str, - github_connection: object, + github_connection: github3.GitHub, ): """ Get all contributors from the organization or repository @@ -123,16 +117,14 @@ def get_all_contributors( all_contributors.append(repo_contributors) # Check for duplicates and merge when usernames are equal - all_contributors = contributor_stats.merge_contributors(all_contributors) - - return all_contributors + return contributor_stats.merge_contributors(all_contributors) def get_contributors( - repo: object, + repo: github3.repos.Repository, start_date: str, end_date: str, -): +) -> list[contributor_stats.ContributorStats] | None: """ Get contributors from a single repository and filter by start end dates if present. @@ -154,9 +146,7 @@ def get_contributors( # Check if user has commits in the date range if start_date and end_date: - user_commits = repo.commits( - author=user.login, since=start_date, until=end_date - ) + user_commits = repo.commits(author=user.login, since=start_date, until=end_date) # If the user has no commits in the date range, skip them try: @@ -168,16 +158,15 @@ def get_contributors( if start_date and end_date: commit_url = f"https://github.com/{repo.full_name}/commits?author={user.login}&since={start_date}&until={end_date}" else: - commit_url = ( - f"https://github.com/{repo.full_name}/commits?author={user.login}" - ) + commit_url = f"https://github.com/{repo.full_name}/commits?author={user.login}" contributor = contributor_stats.ContributorStats( - user.login, - False, - user.avatar_url, - user.contributions_count, - commit_url, - "", + username=user.login, + new_contributor=False, + avatar_url=user.avatar_url, + contribution_count=user.contributions_count, + commit_url=commit_url, + sponsor_info="", + organizations=list(map(lambda x: x.url.split("/")[-1], user.organizations())), ) contributors.append(contributor) except Exception as e: @@ -186,7 +175,3 @@ def get_contributors( return None return contributors - - -if __name__ == "__main__": - main() diff --git a/contributors/__main__.py b/contributors/__main__.py new file mode 100644 index 0000000..0e914be --- /dev/null +++ b/contributors/__main__.py @@ -0,0 +1,3 @@ +from contributors import main + +main() diff --git a/auth.py b/contributors/auth.py similarity index 91% rename from auth.py rename to contributors/auth.py index 825ff14..d4b4fdc 100644 --- a/auth.py +++ b/contributors/auth.py @@ -25,9 +25,7 @@ def auth_to_github( """ if gh_app_id and gh_app_private_key_bytes and gh_app_installation_id: gh = github3.github.GitHub() - gh.login_as_app_installation( - gh_app_private_key_bytes, gh_app_id, gh_app_installation_id - ) + gh.login_as_app_installation(gh_app_private_key_bytes, gh_app_id, gh_app_installation_id) github_connection = gh elif ghe and token: github_connection = github3.github.GitHubEnterprise(ghe, token=token) diff --git a/contributor_stats.py b/contributors/contributor_stats.py similarity index 66% rename from contributor_stats.py rename to contributors/contributor_stats.py index 1d7b1db..137fc5c 100644 --- a/contributor_stats.py +++ b/contributors/contributor_stats.py @@ -12,11 +12,13 @@ # ] +from dataclasses import dataclass, field from typing import List import requests +@dataclass class ContributorStats: """ A class to represent a contributor_stats object correlating to a single contributors stats. @@ -31,47 +33,13 @@ class ContributorStats: """ - def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument - """Create a new contributor_stats object""" - return super().__new__(cls) - - def __init__( - self, - username: str, - new_contributor: bool, - avatar_url: str, - contribution_count: int, - commit_url: str, - sponsor_info: str, - ): - """Initialize the contributor_stats object""" - new_contributor = False - self.username = username - self.new_contributor = new_contributor - self.avatar_url = avatar_url - self.contribution_count = contribution_count - self.commit_url = commit_url - self.sponsor_info = sponsor_info - - def __repr__(self) -> str: - """Return the representation of the contributor_stats object""" - return ( - f"contributor_stats(username={self.username}, " - f"new_contributor={self.new_contributor}, " - f"avatar_url={self.avatar_url}, " - f"contribution_count={self.contribution_count}, commit_url={self.commit_url})" - f"sponsor_info={self.sponsor_info})" - ) - - def __eq__(self, other) -> bool: - """Check if two contributor_stats objects are equal""" - return ( - self.username == other.username - and self.new_contributor == other.new_contributor - and self.avatar_url == other.avatar_url - and self.contribution_count == other.contribution_count - and self.commit_url == other.commit_url - ) + username: str + new_contributor: bool + avatar_url: str + contribution_count: int + commit_url: str + sponsor_info: str + organizations: list[str] = field(default_factory=list) def is_new_contributor(username: str, returning_contributors: list) -> bool: @@ -91,7 +59,9 @@ def is_new_contributor(username: str, returning_contributors: list) -> bool: return True -def merge_contributors(contributors: list) -> list: +def merge_contributors( + contributors: list[list[ContributorStats]], +) -> list[ContributorStats]: """ Merge contributors with the same username from multiple repositories. @@ -109,19 +79,12 @@ def merge_contributors(contributors: list) -> list: for merged_contributor in merged_contributors: if merged_contributor.username == contributor.username: # Merge the contribution counts via addition - merged_contributor.contribution_count += ( - contributor.contribution_count - ) + merged_contributor.contribution_count += contributor.contribution_count # Merge the commit urls via concatenation - merged_contributor.commit_url = ( - merged_contributor.commit_url - + ", " - + contributor.commit_url - ) + merged_contributor.commit_url = merged_contributor.commit_url + ", " + contributor.commit_url # Merge the new_contributor attribute via OR merged_contributor.new_contributor = ( - merged_contributor.new_contributor - or contributor.new_contributor + merged_contributor.new_contributor or contributor.new_contributor ) else: @@ -171,8 +134,6 @@ def get_sponsor_information(contributors: list, token: str) -> list: # if the user has a sponsor page, add it to the contributor object if data["repositoryOwner"]["hasSponsorsListing"]: - contributor.sponsor_info = ( - f"https://github.com/sponsors/{contributor.username}" - ) + contributor.sponsor_info = f"https://github.com/sponsors/{contributor.username}" return contributors diff --git a/env.py b/contributors/env.py similarity index 55% rename from env.py rename to contributors/env.py index 3842ada..e8d0bd2 100644 --- a/env.py +++ b/contributors/env.py @@ -5,6 +5,7 @@ import datetime import os +from dataclasses import dataclass from os.path import dirname, join from dotenv import load_dotenv @@ -64,17 +65,34 @@ def validate_date_format(env_var_name: str) -> str: try: datetime.datetime.strptime(date_to_validate, pattern) except ValueError as exc: - raise ValueError( - f"{env_var_name} environment variable not in the format YYYY-MM-DD" - ) from exc + raise ValueError(f"{env_var_name} environment variable not in the format YYYY-MM-DD") from exc return date_to_validate +@dataclass +class EnvironmentConfig: + """ + Environment Configuration + """ + + organization: str | None + repositories_list: list[str] | None + gh_app_id: int | None + gh_app_installation_id: int | None + gh_app_private_key_bytes: bytes + token: str + ghe: str + start_date: str + end_date: str + sponsor_info: bool + link_to_profile: bool + show_organizations_list: list[str] + filename: str = "contributors" + + def get_env_vars( test: bool = False, -) -> tuple[ - str | None, list[str], int | None, int | None, bytes, str, str, str, str, bool, bool -]: +) -> EnvironmentConfig: """ Get the environment variables for use in the action. @@ -82,17 +100,31 @@ def get_env_vars( None Returns: - str: the organization to get contributor information for - List[str]: A list of the repositories to get contributor information for - int|None: the GitHub App ID to use for authentication - int|None: the GitHub App Installation ID to use for authentication - bytes: the GitHub App Private Key as bytes to use for authentication - str: the GitHub token to use for authentication - str: the GitHub Enterprise URL to use for authentication - str: the start date to get contributor information from - str: the end date to get contributor information to. - str: whether to get sponsor information on the contributor - str: whether to link username to Github profile in markdown output + EnvironmentConfig: + organization, str | None: + the organization to get contributor information for + repositories_list, List[str] | None: + A list of the repositories to get contributor information for + gh_app_id, int|None: + the GitHub App ID to use for authentication + gh_app_installation_id, int|None: + the GitHub App Installation ID to use for authentication + gh_app_private_key_bytes, bytes: + the GitHub App Private Key as bytes to use for authentication + token, str: + the GitHub token to use for authentication + ghe, str: + the GitHub Enterprise URL to use for authentication + start_date, str, + the start date to get contributor information from + end_date, str: + the end date to get contributor information to. + sponsor_info, bool: + whether to get sponsor information on the contributor + link_to_profile, bool: + whether to link username to Github profile in markdown output + show_organizations_list, list: + Organizations to show in order of preference """ @@ -104,29 +136,23 @@ def get_env_vars( repositories_str = os.getenv("REPOSITORY") # Either organization or repository must be set if not organization and not repositories_str: - raise ValueError( - "ORGANIZATION and REPOSITORY environment variables were not set. Please set one" - ) + raise ValueError("ORGANIZATION and REPOSITORY environment variables were not set. Please set one") gh_app_id = get_int_env_var("GH_APP_ID") gh_app_private_key_bytes = os.environ.get("GH_APP_PRIVATE_KEY", "").encode("utf8") gh_app_installation_id = get_int_env_var("GH_APP_INSTALLATION_ID") if gh_app_id and (not gh_app_private_key_bytes or not gh_app_installation_id): - raise ValueError( - "GH_APP_ID set and GH_APP_INSTALLATION_ID or GH_APP_PRIVATE_KEY variable not set" - ) + raise ValueError("GH_APP_ID set and GH_APP_INSTALLATION_ID or GH_APP_PRIVATE_KEY variable not set") token = os.getenv("GH_TOKEN", "") - if ( - not gh_app_id - and not gh_app_private_key_bytes - and not gh_app_installation_id - and not token - ): + if not gh_app_id and not gh_app_private_key_bytes and not gh_app_installation_id and not token: raise ValueError("GH_TOKEN environment variable not set") ghe = os.getenv("GH_ENTERPRISE_URL", default="").strip() + filename = os.getenv("CONTRIB_FILENAME", default="contributors").strip() + + show_organizations = os.getenv("SHOW_ORGANIZATIONS", default="").strip() start_date = validate_date_format("START_DATE") end_date = validate_date_format("END_DATE") @@ -137,11 +163,13 @@ def get_env_vars( # Separate repositories_str into a list based on the comma separator repositories_list = [] if repositories_str: - repositories_list = [ - repository.strip() for repository in repositories_str.split(",") - ] + repositories_list = [repository.strip() for repository in repositories_str.split(",")] + + show_organizations_list = [] + if show_organizations: + show_organizations_list = [org.strip() for org in show_organizations.split(",")] - return ( + return EnvironmentConfig( organization, repositories_list, gh_app_id, @@ -153,4 +181,6 @@ def get_env_vars( end_date, sponsor_info, link_to_profile, + show_organizations_list, + filename=filename, ) diff --git a/json_writer.py b/contributors/json_writer.py similarity index 94% rename from json_writer.py rename to contributors/json_writer.py index 85dc607..be79c42 100644 --- a/json_writer.py +++ b/contributors/json_writer.py @@ -12,6 +12,7 @@ def write_to_json( repository_list, sponsor_info, link_to_profile, + show_organizations_list, ): """Write data to a JSON file. @@ -24,6 +25,7 @@ def write_to_json( repository_list (list): A list of repositories for which the contributors are being listed. sponsor_info (str): A string indicating whether sponsor information should be included. link_to_profile (str): A string indicating whether a link to the contributor's profile should be included. + show_organizations_list (list): Organisations to show Returns: None @@ -70,6 +72,7 @@ def write_to_json( "repository_list": repository_list, "sponsor_info": sponsor_info, "link_to_profile": link_to_profile, + "show_organizations_list": show_organizations_list, "contributors": [contributor.__dict__ for contributor in contributors], } diff --git a/markdown.py b/contributors/markdown.py similarity index 77% rename from markdown.py rename to contributors/markdown.py index 1e13cec..b154fca 100644 --- a/markdown.py +++ b/contributors/markdown.py @@ -2,6 +2,11 @@ """This module contains the functions needed to write the output to markdown files.""" +from collections import defaultdict, OrderedDict + +from .contributor_stats import ContributorStats + + def write_to_markdown( collaborators, filename, @@ -11,6 +16,7 @@ def write_to_markdown( repository, sponsor_info, link_to_profile, + show_organizations_list, ): """ This function writes a list of collaborators to a markdown file in table format. @@ -26,6 +32,7 @@ def write_to_markdown( repository (str): The repository for which the contributors are being listed. sponsor_info (str): True if the user wants the sponsor_url shown in the report link_to_profile (str): True if the user wants the username linked to Github profile in the report + show_organizations_list (list): Organizations to show Returns: None @@ -40,22 +47,17 @@ def write_to_markdown( repository, sponsor_info, link_to_profile, + show_organizations_list, ) # Put together the summary table including # of new contributions, # of new contributors, % new contributors, % returning contributors - summary_table = get_summary_table( - collaborators, start_date, end_date, total_contributions - ) + summary_table = get_summary_table(collaborators, start_date, end_date, total_contributions) # Write the markdown file - write_markdown_file( - filename, start_date, end_date, organization, repository, table, summary_table - ) + write_markdown_file(filename, start_date, end_date, organization, repository, table, summary_table) -def write_markdown_file( - filename, start_date, end_date, organization, repository, table, summary_table -): +def write_markdown_file(filename, start_date, end_date, organization, repository, table, summary_table): """ This function writes all the tables and data to a markdown file with tables to organizae the information. @@ -75,18 +77,23 @@ def write_markdown_file( with open(filename, "w", encoding="utf-8") as markdown_file: markdown_file.write("# Contributors\n\n") if start_date and end_date: - markdown_file.write( - f"- Date range for contributor list: {start_date} to {end_date}\n" - ) + markdown_file.write(f"- Date range for contributor list: {start_date} to {end_date}\n") if organization: markdown_file.write(f"- Organization: {organization}\n") if repository: markdown_file.write(f"- Repository: {repository}\n") markdown_file.write("\n") markdown_file.write(summary_table) - markdown_file.write(table) + if len(table) == 1 and "Independent" in table: + markdown_file.write(table["Independent"]) + else: + # Put independent last + for org in list(table.keys()): + org_title = f"## [{org}](https://github.com/{org})\n" if not org == "Independent" else f"## {org} \n" + markdown_file.write(org_title) + markdown_file.write(table[org]) markdown_file.write( - "\n _this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_\n" + "\n _this file was generated by the [Organizational Contributors GitHub Action](https://github.com/HCookie/organizational_contributors)_\n" ) @@ -109,9 +116,7 @@ def get_summary_table(collaborators, start_date, end_date, total_contributions): summary_table = "| Total Contributors | Total Contributions | % New Contributors |\n| --- | --- | --- |\n" if len(collaborators) > 0: new_contributors_percentage = round( - (len([x for x in collaborators if x.new_contributor is True])) - / len(collaborators) - * 100, + (len([x for x in collaborators if x.new_contributor is True])) / len(collaborators) * 100, 2, ) else: @@ -127,21 +132,20 @@ def get_summary_table(collaborators, start_date, end_date, total_contributions): ) else: summary_table = "| Total Contributors | Total Contributions |\n| --- | --- |\n" - summary_table += ( - "| " + str(len(collaborators)) + " | " + str(total_contributions) + " |\n\n" - ) + summary_table += "| " + str(len(collaborators)) + " | " + str(total_contributions) + " |\n\n" return summary_table def get_contributor_table( - collaborators, + collaborators: list[ContributorStats], start_date, end_date, organization, repository, sponsor_info, link_to_profile, + show_organizations_list, ): """ This function returns a string containing a markdown table of the contributors and the total contribution count. @@ -174,16 +178,20 @@ def get_contributor_table( headers = "| " + " | ".join(columns) + " |\n" headers += "| " + " | ".join(["---"] * len(columns)) + " |\n" - table = headers total_contributions = 0 + organization_contributors = defaultdict(list) + for collaborator in collaborators: total_contributions += collaborator.contribution_count username = collaborator.username contribution_count = collaborator.contribution_count if repository: commit_urls = collaborator.commit_url - if organization: + + if not isinstance(repository, list): + repository = [repository] + if organization or len(repository) > 1: # split the urls from the comma separated list and make them into markdown links commit_url_list = collaborator.commit_url.split(",") commit_urls = "" @@ -205,5 +213,24 @@ def get_contributor_table( row += f" [Sponsor Link]({collaborator.sponsor_info}) |" row += f" {commit_urls} |\n" - table += row - return table, total_contributions + added_to_org: bool = False + + for org in collaborator.organizations or []: + if org in show_organizations_list or "all" in show_organizations_list: + organization_contributors[org].append(row) + added_to_org = True + break + + if not added_to_org: + organization_contributors["Independent"].append(row) + + ordered_orgs = [org for org in (*show_organizations_list, "Independent") if org in organization_contributors] + tables = OrderedDict( + [ + (org, headers + "".join(organization_contributors[org])) + for org in ordered_orgs + ] + ) + + # table += row + return tables, total_contributions diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_auth.py b/tests/test_auth.py similarity index 98% rename from test_auth.py rename to tests/test_auth.py index 69fc9f5..305024e 100644 --- a/test_auth.py +++ b/tests/test_auth.py @@ -3,8 +3,8 @@ import unittest from unittest.mock import MagicMock, patch -import auth import github3.github +from contributors import auth class TestAuth(unittest.TestCase): diff --git a/test_contributor_stats.py b/tests/test_contributor_stats.py similarity index 97% rename from test_contributor_stats.py rename to tests/test_contributor_stats.py index adcd0a5..d43126f 100644 --- a/test_contributor_stats.py +++ b/tests/test_contributor_stats.py @@ -2,7 +2,11 @@ import unittest -from contributor_stats import ContributorStats, is_new_contributor, merge_contributors +from contributors.contributor_stats import ( + ContributorStats, + is_new_contributor, + merge_contributors, +) class TestContributorStats(unittest.TestCase): diff --git a/test_contributors.py b/tests/test_contributors.py similarity index 74% rename from test_contributors.py rename to tests/test_contributors.py index ab75d0c..fc7d6bb 100644 --- a/test_contributors.py +++ b/tests/test_contributors.py @@ -3,7 +3,7 @@ import unittest from unittest.mock import MagicMock, patch -from contributor_stats import ContributorStats +from contributors.contributor_stats import ContributorStats from contributors import get_all_contributors, get_contributors @@ -28,12 +28,13 @@ def test_get_contributors(self, mock_contributor_stats): get_contributors(mock_repo, "2022-01-01", "2022-12-31") mock_contributor_stats.assert_called_once_with( - "user", - False, - "https://avatars.githubusercontent.com/u/12345678?v=4", - 100, - "https://github.com/owner/repo/commits?author=user&since=2022-01-01&until=2022-12-31", - "", + username="user", + new_contributor=False, + avatar_url="https://avatars.githubusercontent.com/u/12345678?v=4", + contribution_count=100, + commit_url="https://github.com/owner/repo/commits?author=user&since=2022-01-01&until=2022-12-31", + sponsor_info="", + organizations=[], ) @patch("contributors.get_contributors") @@ -54,23 +55,23 @@ def test_get_all_contributors_with_organization(self, mock_get_contributors): 100, "commit_url", "sponsor_url_1", + [], ), ] - result = get_all_contributors( - "org", "", "2022-01-01", "2022-12-31", mock_github_connection - ) + result = get_all_contributors("org", "", "2022-01-01", "2022-12-31", mock_github_connection) self.assertEqual( result, [ ContributorStats( - "user", - False, - "https://avatars.githubusercontent.com/u/29484535?v=4", - 200, - "commit_url, commit_url", - "sponsor_url_1", + username="user", + new_contributor=False, + avatar_url="https://avatars.githubusercontent.com/u/29484535?v=4", + contribution_count=200, + commit_url="commit_url, commit_url", + sponsor_info="sponsor_url_1", + organizations=[], ), ], ) @@ -95,9 +96,7 @@ def test_get_all_contributors_with_repository(self, mock_get_contributors): ) ] - result = get_all_contributors( - "", ["owner/repo"], "2022-01-01", "2022-12-31", mock_github_connection - ) + result = get_all_contributors("", ["owner/repo"], "2022-01-01", "2022-12-31", mock_github_connection) self.assertEqual( result, @@ -112,9 +111,7 @@ def test_get_all_contributors_with_repository(self, mock_get_contributors): ), ], ) - mock_get_contributors.assert_called_once_with( - "repo", "2022-01-01", "2022-12-31" - ) + mock_get_contributors.assert_called_once_with("repo", "2022-01-01", "2022-12-31") @patch("contributors.contributor_stats.ContributorStats") def test_get_contributors_skip_users_with_no_commits(self, mock_contributor_stats): @@ -139,12 +136,13 @@ def test_get_contributors_skip_users_with_no_commits(self, mock_contributor_stat # Note that only user is returned and user2 is not returned here because there were no commits in the date range mock_contributor_stats.assert_called_once_with( - "user", - False, - "https://avatars.githubusercontent.com/u/12345678?v=4", - 100, - "https://github.com/owner/repo/commits?author=user&since=2022-01-01&until=2022-12-31", - "", + username="user", + new_contributor=False, + avatar_url="https://avatars.githubusercontent.com/u/12345678?v=4", + contribution_count=100, + commit_url="https://github.com/owner/repo/commits?author=user&since=2022-01-01&until=2022-12-31", + sponsor_info="", + organizations=[], ) diff --git a/test_env.py b/tests/test_env.py similarity index 69% rename from test_env.py rename to tests/test_env.py index 7632c41..73486c4 100644 --- a/test_env.py +++ b/tests/test_env.py @@ -4,7 +4,7 @@ import unittest from unittest.mock import patch -import env +from contributors import env class TestEnv(unittest.TestCase): @@ -51,31 +51,19 @@ def test_get_env_vars(self): Test the get_env_vars function when all environment variables are set correctly. """ - ( - organization, - repository_list, - gh_app_id, - gh_app_installation_id, - gh_app_private_key_bytes, - token, - ghe, - start_date, - end_date, - sponsor_info, - link_to_profile, - ) = env.get_env_vars() - - self.assertEqual(organization, "org") - self.assertEqual(repository_list, ["repo", "repo2"]) - self.assertIsNone(gh_app_id) - self.assertIsNone(gh_app_installation_id) - self.assertEqual(gh_app_private_key_bytes, b"") - self.assertEqual(token, "token") - self.assertEqual(ghe, "") - self.assertEqual(start_date, "2022-01-01") - self.assertEqual(end_date, "2022-12-31") - self.assertFalse(sponsor_info) - self.assertTrue(link_to_profile) + environment = env.get_env_vars() + + self.assertEqual(environment.organization, "org") + self.assertEqual(environment.repositories_list, ["repo", "repo2"]) + self.assertIsNone(environment.gh_app_id) + self.assertIsNone(environment.gh_app_installation_id) + self.assertEqual(environment.gh_app_private_key_bytes, b"") + self.assertEqual(environment.token, "token") + self.assertEqual(environment.ghe, "") + self.assertEqual(environment.start_date, "2022-01-01") + self.assertEqual(environment.end_date, "2022-12-31") + self.assertFalse(environment.sponsor_info) + self.assertTrue(environment.link_to_profile) @patch.dict( os.environ, @@ -159,31 +147,19 @@ def test_get_env_vars_no_dates(self): and start_date and end_date are not set. """ - ( - organization, - repository_list, - gh_app_id, - gh_app_installation_id, - gh_app_private_key_bytes, - token, - ghe, - start_date, - end_date, - sponsor_info, - link_to_profile, - ) = env.get_env_vars() - - self.assertEqual(organization, "org") - self.assertEqual(repository_list, ["repo", "repo2"]) - self.assertIsNone(gh_app_id) - self.assertIsNone(gh_app_installation_id) - self.assertEqual(gh_app_private_key_bytes, b"") - self.assertEqual(token, "token") - self.assertEqual(ghe, "") - self.assertEqual(start_date, "") - self.assertEqual(end_date, "") - self.assertFalse(sponsor_info) - self.assertTrue(link_to_profile) + environment = env.get_env_vars() + + self.assertEqual(environment.organization, "org") + self.assertEqual(environment.repositories_list, ["repo", "repo2"]) + self.assertIsNone(environment.gh_app_id) + self.assertIsNone(environment.gh_app_installation_id) + self.assertEqual(environment.gh_app_private_key_bytes, b"") + self.assertEqual(environment.token, "token") + self.assertEqual(environment.ghe, "") + self.assertEqual(environment.start_date, "") + self.assertEqual(environment.end_date, "") + self.assertFalse(environment.sponsor_info) + self.assertTrue(environment.link_to_profile) if __name__ == "__main__": diff --git a/test_env_get_bool.py b/tests/test_env_get_bool.py similarity index 97% rename from test_env_get_bool.py rename to tests/test_env_get_bool.py index 3165de1..80bb0f7 100644 --- a/test_env_get_bool.py +++ b/tests/test_env_get_bool.py @@ -4,7 +4,7 @@ import unittest from unittest.mock import patch -from env import get_bool_env_var +from contributors.env import get_bool_env_var class TestEnv(unittest.TestCase): diff --git a/test_json_writer.py b/tests/test_json_writer.py similarity index 89% rename from test_json_writer.py rename to tests/test_json_writer.py index 1071ea1..b8dc96c 100644 --- a/test_json_writer.py +++ b/tests/test_json_writer.py @@ -4,8 +4,8 @@ import os import unittest -from contributor_stats import ContributorStats -from json_writer import write_to_json +from contributors.contributor_stats import ContributorStats +from contributors.json_writer import write_to_json class TestWriteToJson(unittest.TestCase): @@ -27,10 +27,12 @@ def setUp(self): "new_contributor": False, "avatar_url": "https://test_url.com", "contribution_count": 10, + "organizations": [], "commit_url": "https://test_commit_url.com", "sponsor_info": "", } ], + "show_organizations_list": [], } def test_write_to_json(self): @@ -55,6 +57,7 @@ def test_write_to_json(self): repository_list=self.data["repository_list"], sponsor_info=self.data["sponsor_info"], link_to_profile=self.data["link_to_profile"], + show_organizations_list=[] ) with open(self.filename, "r", encoding="utf-8") as f: result = json.load(f) diff --git a/test_markdown.py b/tests/test_markdown.py similarity index 91% rename from test_markdown.py rename to tests/test_markdown.py index f4e46cb..5070338 100644 --- a/test_markdown.py +++ b/tests/test_markdown.py @@ -3,8 +3,8 @@ import unittest from unittest.mock import mock_open, patch -import contributor_stats -from markdown import write_to_markdown +from contributors import contributor_stats +from contributors.markdown import write_to_markdown class TestMarkdown(unittest.TestCase): @@ -49,13 +49,12 @@ def test_write_to_markdown(self, mock_file): "org/repo", "false", "true", + [], ) mock_file.assert_called_once_with("filename", "w", encoding="utf-8") mock_file().write.assert_any_call("# Contributors\n\n") - mock_file().write.assert_any_call( - "- Date range for contributor list: 2023-01-01 to 2023-01-02\n" - ) + mock_file().write.assert_any_call("- Date range for contributor list: 2023-01-01 to 2023-01-02\n") mock_file().write.assert_any_call( "| Total Contributors | Total Contributions | % New Contributors |\n| --- | --- | --- |\n| 2 | 300 | 50.0% |\n\n" ) @@ -103,13 +102,12 @@ def test_write_to_markdown_with_sponsors(self, mock_file): "org/repo", "true", "true", + [], ) mock_file.assert_called_once_with("filename", "w", encoding="utf-8") mock_file().write.assert_any_call("# Contributors\n\n") - mock_file().write.assert_any_call( - "- Date range for contributor list: 2023-01-01 to 2023-01-02\n" - ) + mock_file().write.assert_any_call("- Date range for contributor list: 2023-01-01 to 2023-01-02\n") mock_file().write.assert_any_call( "| Total Contributors | Total Contributions | % New Contributors |\n| --- | --- | --- |\n| 2 | 300 | 50.0% |\n\n" ) @@ -157,13 +155,12 @@ def test_write_to_markdown_without_link_to_profile(self, mock_file): "org/repo", "false", "false", + [], ) mock_file.assert_called_once_with("filename", "w", encoding="utf-8") mock_file().write.assert_any_call("# Contributors\n\n") - mock_file().write.assert_any_call( - "- Date range for contributor list: 2023-01-01 to 2023-01-02\n" - ) + mock_file().write.assert_any_call("- Date range for contributor list: 2023-01-01 to 2023-01-02\n") mock_file().write.assert_any_call( "| Total Contributors | Total Contributions | % New Contributors |\n| --- | --- | --- |\n| 2 | 300 | 50.0% |\n\n" )