├── .all-contributorsrc
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
├── pull_request_template.md
└── workflows
│ ├── checks.yml
│ ├── gh-shell.yml
│ └── pypi_publish.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── assets
├── daterange.png
├── grid.png
├── lang.png
├── list.png
├── starcli-demo2.gif
├── starcli-small-cover.png
├── starcli_cover.PNG
└── table.png
├── requirements.txt
├── requirements_dev.txt
├── setup.py
├── starcli
├── __init__.py
├── __main__.py
├── layouts.py
├── search.py
└── spoken-languages.json
└── tests
├── conftest.py
├── test_cli.py
├── test_search.py
└── test_shorten_count.py
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "commit": false,
7 | "contributors": [
8 | {
9 | "login": "Shagilton",
10 | "name": "Shagilton",
11 | "avatar_url": "https://avatars0.githubusercontent.com/u/21122143?v=4",
12 | "profile": "https://github.com/Shagilton",
13 | "contributions": [
14 | "code"
15 | ]
16 | },
17 | {
18 | "login": "hexbee",
19 | "name": "hexbee",
20 | "avatar_url": "https://avatars2.githubusercontent.com/u/26668583?v=4",
21 | "profile": "https://github.com/hexbee",
22 | "contributions": [
23 | "bug"
24 | ]
25 | },
26 | {
27 | "login": "swellander",
28 | "name": "Sam Wellander",
29 | "avatar_url": "https://avatars0.githubusercontent.com/u/22231097?v=4",
30 | "profile": "https://github.com/swellander",
31 | "contributions": [
32 | "code"
33 | ]
34 | },
35 | {
36 | "login": "shivam212",
37 | "name": "Shivam Sinha",
38 | "avatar_url": "https://avatars0.githubusercontent.com/u/32016929?v=4",
39 | "profile": "https://www.shivamsinha.xyz/",
40 | "contributions": [
41 | "code"
42 | ]
43 | },
44 | {
45 | "login": "willmcgugan",
46 | "name": "Will McGugan",
47 | "avatar_url": "https://avatars3.githubusercontent.com/u/554369?v=4",
48 | "profile": "https://www.willmcgugan.com",
49 | "contributions": [
50 | "code"
51 | ]
52 | },
53 | {
54 | "login": "ashikjm",
55 | "name": "Ashik J M",
56 | "avatar_url": "https://avatars1.githubusercontent.com/u/12744524?v=4",
57 | "profile": "https://github.com/ashikjm",
58 | "contributions": [
59 | "code"
60 | ]
61 | },
62 | {
63 | "login": "ylchao",
64 | "name": "Yu-Lin Chao",
65 | "avatar_url": "https://avatars0.githubusercontent.com/u/15059429?v=4",
66 | "profile": "https://github.com/ylchao",
67 | "contributions": [
68 | "code"
69 | ]
70 | },
71 | {
72 | "login": "Saif807380",
73 | "name": "Saif Kazi",
74 | "avatar_url": "https://avatars2.githubusercontent.com/u/50794619?v=4",
75 | "profile": "https://github.com/Saif807380",
76 | "contributions": [
77 | "code",
78 | "doc"
79 | ]
80 | },
81 | {
82 | "login": "arcanearronax",
83 | "name": "arcanearronax",
84 | "avatar_url": "https://avatars3.githubusercontent.com/u/16456078?v=4",
85 | "profile": "http://arcanedomain.duckdns.org",
86 | "contributions": [
87 | "test",
88 | "code"
89 | ]
90 | },
91 | {
92 | "login": "jSadoski",
93 | "name": "jSadoski",
94 | "avatar_url": "https://avatars1.githubusercontent.com/u/1865629?v=4",
95 | "profile": "https://github.com/jSadoski",
96 | "contributions": [
97 | "doc",
98 | "code"
99 | ]
100 | },
101 | {
102 | "login": "odmishien",
103 | "name": "odmishien(Tetsuya MISHIMA)",
104 | "avatar_url": "https://avatars3.githubusercontent.com/u/25533384?v=4",
105 | "profile": "https://www.odmishien.fun",
106 | "contributions": [
107 | "code"
108 | ]
109 | },
110 | {
111 | "login": "ineelshah",
112 | "name": "Neel Shah",
113 | "avatar_url": "https://avatars1.githubusercontent.com/u/40118578?v=4",
114 | "profile": "http://linkedin.com/in/ineelshah",
115 | "contributions": [
116 | "code"
117 | ]
118 | },
119 | {
120 | "login": "0xflotus",
121 | "name": "0xflotus",
122 | "avatar_url": "https://avatars3.githubusercontent.com/u/26602940?v=4",
123 | "profile": "https://github.com/0xflotus",
124 | "contributions": [
125 | "code"
126 | ]
127 | },
128 | {
129 | "login": "AkashD-Developer",
130 | "name": "Akash Dhanwani",
131 | "avatar_url": "https://avatars.githubusercontent.com/u/44431401?v=4",
132 | "profile": "https://github.com/AkashD-Developer",
133 | "contributions": [
134 | "code"
135 | ]
136 | },
137 | {
138 | "login": "davised",
139 | "name": "Ed Davis",
140 | "avatar_url": "https://avatars.githubusercontent.com/u/16343359?v=4",
141 | "profile": "http://cqls.oregonstate.edu",
142 | "contributions": [
143 | "code"
144 | ]
145 | },
146 | {
147 | "login": "tizee",
148 | "name": "Jeff Chiang",
149 | "avatar_url": "https://avatars.githubusercontent.com/u/33030965?v=4",
150 | "profile": "https://jeetizee.com",
151 | "contributions": [
152 | "code"
153 | ]
154 | },
155 | {
156 | "login": "dzmitry-kankalovich",
157 | "name": "Dmitry Kankalovich",
158 | "avatar_url": "https://avatars.githubusercontent.com/u/6346981?v=4",
159 | "profile": "https://dmitrykankalovich.com/",
160 | "contributions": [
161 | "code"
162 | ]
163 | }
164 | ],
165 | "contributorsPerLine": 7,
166 | "projectName": "starcli",
167 | "projectOwner": "hedyhli",
168 | "repoType": "github",
169 | "repoHost": "https://github.com",
170 | "skipCi": true,
171 | "commitConvention": "angular"
172 | }
173 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: #hedyli
2 | issuehunt: hedyhli/starcli
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: 'Bug: '
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
12 |
18 |
19 |
20 |
21 | **Python version**
22 |
23 |
24 |
25 | **your operating system (and terminal type or shell of needed)**
26 |
27 |
28 | **starcli version**
29 |
30 |
36 |
37 |
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "Feat:"
5 | labels: enhancement
6 | assignees: ''
7 | ---
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: pip
4 | directory: "/"
5 | schedule:
6 | interval: weekly
7 | time: "11:00"
8 | open-pull-requests-limit: 10
9 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
3 |
4 | **Checklist**
5 |
6 | - [ ] My code is properly formatted using the latest [black](https://github.com/psf/black)
7 | - [ ] I have added/updated [tests](https://github.com/hedythedev/starcli/tree/main/tests) if needed
8 | - [ ] I have tried running my code manually
9 | - [ ] I have checked for spelling errors
10 |
14 |
15 |
16 | **Description**
17 |
18 |
19 |
25 |
26 |
--------------------------------------------------------------------------------
/.github/workflows/checks.yml:
--------------------------------------------------------------------------------
1 | name: checks
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 |
10 | jobs:
11 | lint:
12 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
13 | runs-on: ubuntu-latest
14 | strategy:
15 | matrix:
16 | python-version: [3.7, 3.8, 3.9, '3.10', '3.11']
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 | - name: Set up Python ${{ matrix.python-version }}
21 | uses: actions/setup-python@v2
22 | with:
23 | python-version: ${{ matrix.python-version }}
24 | - name: Install dependencies
25 | run: |
26 | python -m pip install --upgrade pip
27 | pip install pylint pytest black codespell
28 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
29 | - name: GitHub Action for pylint
30 | uses: cclauss/GitHub-Action-for-pylint@0.7.0
31 | - run: black . --check || true
32 | - uses: codespell-project/actions-codespell@master
33 | with:
34 | skip: "*.json,*.png,*.gif,*.PNG,.git"
35 | - run: pip install -r requirements.txt || true
36 | - run: python -m pytest --auth=${{secrets.STARCLI_AUTH}}
37 |
--------------------------------------------------------------------------------
/.github/workflows/gh-shell.yml:
--------------------------------------------------------------------------------
1 | name: "Shell"
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | command:
6 | description: 'command'
7 | required: true
8 | jobs:
9 | run:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: Set up Python
14 | uses: actions/setup-python@v2
15 | with:
16 | python-version: 3.6
17 | - run: ${{ github.event.inputs.command }}
18 | env:
19 | GIT_COMMITTER_NAME: GitHub Actions
20 | GIT_AUTHOR_NAME: GitHub Actions
21 | EMAIL: github-actions[bot]@users.noreply.github.com
22 |
--------------------------------------------------------------------------------
/.github/workflows/pypi_publish.yml:
--------------------------------------------------------------------------------
1 | # This workflows will upload a Python Package using Twine when a release is created
2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3 |
4 | name: Upload Python Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 |
10 | jobs:
11 | deploy:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Set up Python
18 | uses: actions/setup-python@v2
19 | with:
20 | python-version: '3.7'
21 | - name: Install dependencies
22 | run: |
23 | python -m pip install --upgrade pip
24 | pip install setuptools wheel twine
25 | - name: Build and publish
26 | env:
27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
29 | run: |
30 | python setup.py sdist bdist_wheel
31 | twine upload dist/*
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/python
3 | # Edit at https://www.gitignore.io/?templates=python
4 |
5 | ### Python ###
6 | # Byte-compiled / optimized / DLL files
7 | __pycache__/
8 | *.py[cod]
9 | *$py.class
10 |
11 | # C extensions
12 | *.so
13 |
14 | # Distribution / packaging
15 | .Python
16 | build/
17 | develop-eggs/
18 | dist/
19 | downloads/
20 | eggs/
21 | .eggs/
22 | lib/
23 | lib64/
24 | parts/
25 | sdist/
26 | var/
27 | wheels/
28 | pip-wheel-metadata/
29 | share/python-wheels/
30 | *.egg-info/
31 | .installed.cfg
32 | *.egg
33 | MANIFEST
34 |
35 | # PyInstaller
36 | # Usually these files are written by a python script from a template
37 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
38 | *.manifest
39 | *.spec
40 |
41 | # Installer logs
42 | pip-log.txt
43 | pip-delete-this-directory.txt
44 |
45 | # Unit test / coverage reports
46 | htmlcov/
47 | .tox/
48 | .nox/
49 | .coverage
50 | .coverage.*
51 | .cache
52 | nosetests.xml
53 | coverage.xml
54 | *.cover
55 | .hypothesis/
56 | .pytest_cache/
57 |
58 | # Translations
59 | *.mo
60 | *.pot
61 |
62 | # Scrapy stuff:
63 | .scrapy
64 |
65 | # Sphinx documentation
66 | docs/_build/
67 |
68 | # PyBuilder
69 | target/
70 |
71 | # pyenv
72 | .python-version
73 |
74 | # pipenv
75 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
76 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
77 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
78 | # install all needed dependencies.
79 | #Pipfile.lock
80 |
81 | # celery beat schedule file
82 | celerybeat-schedule
83 |
84 | # SageMath parsed files
85 | *.sage.py
86 |
87 | # Spyder project settings
88 | .spyderproject
89 | .spyproject
90 |
91 | # Rope project settings
92 | .ropeproject
93 |
94 | # Mr Developer
95 | .mr.developer.cfg
96 | .project
97 | .pydevproject
98 |
99 | # mkdocs documentation
100 | /site
101 |
102 | # mypy
103 | .mypy_cache/
104 | .dmypy.json
105 | dmypy.json
106 |
107 | # Pyre type checker
108 | .pyre/
109 |
110 | # End of https://www.gitignore.io/api/python
111 |
112 | .vscode/
113 | venv/
114 | .vim/
115 | # Byte-compiled / optimized / DLL files
116 | __pycache__/
117 | *.py[cod]
118 | *$py.class
119 |
120 | # C extensions
121 | *.so
122 |
123 | # Distribution / packaging
124 | .Python
125 | build/
126 | develop-eggs/
127 | dist/
128 | downloads/
129 | eggs/
130 | .eggs/
131 | lib/
132 | lib64/
133 | parts/
134 | sdist/
135 | var/
136 | wheels/
137 | share/python-wheels/
138 | *.egg-info/
139 | .installed.cfg
140 | *.egg
141 | MANIFEST
142 |
143 | # PyInstaller
144 | # Usually these files are written by a python script from a template
145 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
146 | *.manifest
147 | *.spec
148 |
149 | # Installer logs
150 | pip-log.txt
151 | pip-delete-this-directory.txt
152 |
153 | # Unit test / coverage reports
154 | htmlcov/
155 | .tox/
156 | .nox/
157 | .coverage
158 | .coverage.*
159 | .cache
160 | nosetests.xml
161 | coverage.xml
162 | *.cover
163 | *.py,cover
164 | .hypothesis/
165 | .pytest_cache/
166 | cover/
167 |
168 | # Translations
169 | *.mo
170 | *.pot
171 |
172 | # Django stuff:
173 | *.log
174 | local_settings.py
175 | db.sqlite3
176 | db.sqlite3-journal
177 |
178 | # Flask stuff:
179 | instance/
180 | .webassets-cache
181 |
182 | # Scrapy stuff:
183 | .scrapy
184 |
185 | # Sphinx documentation
186 | docs/_build/
187 |
188 | # PyBuilder
189 | .pybuilder/
190 | target/
191 |
192 | # Jupyter Notebook
193 | .ipynb_checkpoints
194 |
195 | # IPython
196 | profile_default/
197 | ipython_config.py
198 |
199 | # pyenv
200 | # For a library or package, you might want to ignore these files since the code is
201 | # intended to run in multiple environments; otherwise, check them in:
202 | # .python-version
203 |
204 | # pipenv
205 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
206 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
207 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
208 | # install all needed dependencies.
209 | #Pipfile.lock
210 |
211 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
212 | __pypackages__/
213 |
214 | # Celery stuff
215 | celerybeat-schedule
216 | celerybeat.pid
217 |
218 | # SageMath parsed files
219 | *.sage.py
220 |
221 | # Environments
222 | .env
223 | .venv
224 | env/
225 | venv/
226 | ENV/
227 | env.bak/
228 | venv.bak/
229 |
230 | # Spyder project settings
231 | .spyderproject
232 | .spyproject
233 |
234 | # Rope project settings
235 | .ropeproject
236 |
237 | # mkdocs documentation
238 | /site
239 |
240 | # mypy
241 | .mypy_cache/
242 | .dmypy.json
243 | dmypy.json
244 |
245 | # Pyre type checker
246 | .pyre/
247 |
248 | # pytype static type analyzer
249 | .pytype/
250 |
251 | # Cython debug symbols
252 | cython_debug/
253 |
254 | # IDE specific
255 | .DS_Store
256 | .idea/
257 |
258 | # Cache file
259 | .cached_result.json
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to make participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socioeconomic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies within all project spaces, and it also applies when
49 | an individual is representing the project or its community in public spaces.
50 | Examples of representing a project or community include using an official
51 | project e-mail address, posting via an official social media account, or acting
52 | as an appointed representative at an online or offline event. Representation of
53 | a project may be further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 |
4 | **Working on your first Pull Request?** You can learn how from this free
5 | series [How to Contribute to an Open Source Project on
6 | GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github)
7 |
8 |
9 | Thanks for taking the time to look at `CONTRIBUTING.md`.
10 |
11 | All contributions to this project should follow the `CODE_OF_CONDUCT.md`.
12 |
13 | ### Reporting issues and providing feedback
14 |
15 | If you found any issues or bugs, be sure to open up an issue so someone can
16 | check it out!
17 |
18 |
19 | ### Opening a pull request
20 |
21 | Once you've worked on your feature/bugfix etc, you can open a pull request using
22 | the `main` branch as the base branch. Write a clear and concise PR title, and a
23 | detailed description of why you made the change, whether it is related to any
24 | issues etc. And I will review it as soon as I can.
25 |
26 | ### Setting up development environment
27 |
28 | This project is written in Python, requires **Python 3.6 or higher**, and uses
29 | `pip` with `setup.py`.
30 |
31 | To set it up, just [fork](https://github.com/hedyhli/starcli/fork) + clone it,
32 | create a [virtual environment](https://virtualenv.pypa.io/en/latest/) and
33 | install all the dependencies:
34 |
35 | ```bash
36 | $ pip install -r requirements_dev.txt
37 | ```
38 |
39 | The command will install all the requirements needed to run starcli, as well as
40 | dev-dependencies like [black](https://github.com/psf/black),
41 | [pylint](https://www.pylint.org/),
42 | [codespell](https://github.com/codespell-project/codespell) and
43 | [pytest](https://pytest.org).
44 |
45 | > Remember to use the `python3` and `pip3` commands instead of `python` and
46 | > `pip` if your system also has Python 2 installed.
47 |
48 | Alternatively, if you're going to use `pipenv`, you will need to use the `--pre`
49 | flag when installing in order for `black` to work:
50 |
51 | ```bash
52 | $ pipenv install -r requirements_dev.txt --pre
53 | ```
54 |
55 | Check if the setup worked by running starcli from your local folder.
56 |
57 | ```bash
58 | $ python -m starcli --help
59 | ```
60 |
61 | If the above command displayed the help and usage, you are good to go 👍 you can
62 | also test all the other features like list and table output, debug, etc.
63 |
64 | **Running tests**
65 | ```bash
66 | python -m pytest
67 | ```
68 |
69 | or, for authenticated requests:
70 |
71 | ```bash
72 | python -m pytest --auth username:token
73 | ```
74 |
75 | Where **username** is your GitHub username and **token** is a
76 | [personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
77 | Authenticating your request will allow you to test authentication features and
78 | use a higher rate limit with the GitHub API. *This token does not need any permissions!*
79 |
80 | Note that if the the value passed to `--auth` is invalid, some tests will fail.
81 | So check with `starcli --auth` if some tests are mysteriously failing when using
82 | `--auth`.
83 |
84 | **Linting checks**
85 |
86 | ```bash
87 | pylint *.py
88 | ```
89 |
90 | **Formatting & code spell**
91 | ```bash
92 | black . && codespell --skip=".git,*.json,demo-pics/,venv/"
93 | ```
94 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-2024 hedy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ```
2 | ___ ______ _ ____ ____ _ _
3 | / \ * | | /\ | | \ / | * | \ | \
4 | \ \ | | /\ \ | |* / | | | | | |
5 | \ \ | | /--\ \ | | \ | | | | | |
6 | *_\./ ._| / ._\ |_| \_ \.|__. |_|_. |_|
7 |
8 | ```
9 |
10 | Browse trending projects on Github from your command line `$ _`
11 |
12 |
13 | 
14 | [](https://pypi.org/project/starcli/)
15 | [](https://pypi.org/project/starcli/)
16 | [](https://pypi.org/project/starcli/)
17 | [](https://github.com/psf/black)
18 | [](https://github.com/hedyhli/starcli/blob/main/LICENSE)
19 |
20 |
21 |
22 |
23 | 
24 |
25 | ## Features
26 |
27 | * Filters
28 | * Stars
29 | * Pushed date
30 | * Created date
31 | * User
32 | * Topic
33 | * Language
34 | * Spoken language
35 | * Use generic GitHub search API or GitHub trending
36 | * Auth token (optional)
37 | * Paged output
38 | * Different layouts
39 |
40 |
41 | ## Prerequisites
42 |
43 | * Requires Python 3.6 or greater
44 |
45 | ## Installation
46 |
47 | ```sh
48 | pip3 install starcli
49 | ```
50 |
51 | ## Usage
52 |
53 | ```
54 | Usage: starcli [OPTIONS]
55 |
56 | Search and query GitHub repositories
57 |
58 | Options:
59 | -l, --lang TEXT Language filter eg: python. (can be used
60 | multiple times)
61 | -S, --spoken-language TEXT Spoken Language filter eg: en for English,
62 | zh for Chinese
63 | -c, --created TEXT Specify repo creation date in YYYY-MM-DD,
64 | use >date, <=date etc to be more specific.
65 | -t, --topic TEXT Date of last push in YYYY-MM-DD (>, <, >=,
66 | <= specifiers supported)
67 | -p, --pushed TEXT Specify date of last push in YYYY-MM-DD, >=<
68 | allowed
69 | -L, --layout [list|table|grid] The output format (list, table, or grid),
70 | default is list
71 | -s, --stars TEXT Number of stars, default is '>=100'. eg:
72 | '>0', '123', '<50000
73 | -n, --num-results INTEGER The number of items in the results. Default:
74 | 7
75 | -o, --order [desc|asc] Order of repos by stars, 'desc' or 'asc',
76 | default: desc
77 | --long-stats Print the actual stats number (1300 instead
78 | of 1.3k)
79 | -d, --date-range [day|week|month]
80 | View stars received within time, choose
81 | from: day, week, month. Uses GitHub trending
82 | for fetching results, hence some other
83 | filter options may not work.
84 | -u, --user TEXT Filter for trending repositories by username
85 | --auth TEXT Optionally use GitHub personal access token
86 | in the format 'username:password'.
87 | -P, --pager Use $PAGER to page output. (put -r in $LESS
88 | to enable ANSI styles)
89 | --debug Turn on debugging mode
90 | --help Show this message and exit.
91 | ```
92 |
93 |
94 | ### Layouts
95 |
96 | Switch layouts using `--layout {list|table|grid}`, or use the short option `-L`
97 |
98 | **list**
99 |
100 |
101 |
102 | **table**
103 |
104 |
105 |
106 | **grid**
107 |
108 |
109 |
110 | All three of the layout options support clickable links for repository names. If
111 | your terminal supports links, you can directly click on the name and it will
112 | take you to the GitHub repository in your browser.
113 |
114 |
115 | ### Filtering by language
116 |
117 | For example, you only want to find popular Python repos: using `--lang` or `-l`:
118 |
119 | ```
120 | starcli --lang python
121 | ```
122 |
123 | Here's another example `starcli -l python -L grid`, which is python with grid
124 | layout:
125 |
126 |
127 |
128 | ### Filtering by spoken language
129 |
130 | If you wanted to find repos in your native language, you can use
131 | `--spoken-language` or `-S`:
132 |
133 | ```
134 | starcli --spoken-language zh
135 | ```
136 |
137 | The above command lists down repos written in Chinese.
138 |
139 | A full list of language codes is available
140 | [here](./starcli/spoken-languages.json)
141 |
142 | Note that (as with `--date-range`) options like `--topics`, `--pushed`,
143 | `--created` won't take effect because `-d` uses a different search mechanism to
144 | find results.
145 |
146 | ### Specify the number (or range) of stars
147 |
148 | (Recommended to be used with `--created`)
149 |
150 | The default range is >=100 stars.
151 |
152 | Use `--stars` or `-s` to specify what you want, for example, if you want to find
153 | repos that has more than 100 stars, you can use:
154 |
155 | ```
156 | starcli -s '>100'
157 | ```
158 |
159 | Note that if you do something like `>1000` not many repos can have more than
160 | 1000 and is created within around 200 days (which is the default for
161 | `--created`), to specify date of creation, use `--created`, see below.
162 |
163 | ### Filter by stars daily, weekly or monthly
164 |
165 | You can view the number of stars a repo received today, this week or this month
166 | by using the `--date-range` or `-d` option:
167 |
168 | ```
169 | starcli -d this-week -L table
170 | ```
171 |
172 | This command will also display the number of stars received for each repo this
173 | week in the form of a table.
174 |
175 | `-d` uses GitHub Trending search for repositories, hence options `--topic`,
176 | `--pushed`, `--created` won't take effect.
177 |
178 | ### Specify the date of creation
179 |
180 | `--created`/`-c` accepts a date in ISO8601 format: yyyy-mm-dd
181 |
182 | For example, for repos created on 1st January 2014, use:
183 | ```
184 | starcli --created 2014-01-01
185 | ```
186 |
187 | To search for repos that are created *on or after* 1st January 2014, use:
188 | ```
189 | starcli --created '>=2014-01-01'
190 | ```
191 |
192 | ### Filtering by topics
193 |
194 | This option lets you filter by topics. You can use `--topics` or `-t` to include
195 | a topic in search.
196 |
197 | This option can be used multiple times.
198 |
199 | ```
200 | starcli -l python -d 2020-07-06 -t deep-learning -t pytorch
201 | ```
202 |
203 | ### Specifying last pushed date
204 |
205 | Use `--pushed`/`-p` when you want to find popular repos that are last updated on
206 | a given date, say 2020-01-01 for 1st of Jan 2020:
207 |
208 | ```
209 | starcli -p 2020-01-01
210 | ```
211 |
212 | You can also prefix the value with ">=<" like:
213 |
214 | ```
215 | starcli -p '>=2020-01-01'
216 | ```
217 |
218 | This is find repos that have last pushed after or on January the 1st, 2020.
219 |
220 | Read more about the >=< syntax on [GitHub
221 | Docs](https://docs.github.com/en/github/searching-for-information-on-github/understanding-the-search-syntax#query-for-values-greater-or-less-than-another-value).
222 |
223 | ### Searching by user
224 |
225 | Recommended to be used with `--stars` and/or `--date-created`.
226 |
227 | Finding trending projects by GitHub username is supported too. Use `--user` or
228 | `-u` to do so.
229 |
230 | Just provide a valid GitHub username after it, like:
231 |
232 | ```
233 | starcli -u torvalds
234 | starcli -u gvanrossum
235 | ```
236 |
237 | ### Using date ranges
238 |
239 | You can use `--date-range` or `-d` and specify today, this-week, or this-month,
240 | so that GitHub Trending search function will be used to find popular repos and
241 | tell you how much stars are gained this day/week/month depending on the option
242 | you used.
243 |
244 | ```
245 | starcli -d this-week
246 | ```
247 |
248 |
249 |
250 | Note that (like `--spoken-language`) options like `--topics`, `--pushed`,
251 | `--created` won't take effect because `-d` uses a different search mechanism to
252 | find results.
253 |
254 | ### Limit the number of results shown
255 |
256 | Don't like the default 7? You can change it to something else, using
257 | `--limit-results` or `-r` followed by an integer:
258 |
259 | ```
260 | starcli -r 2
261 | ```
262 |
263 | The above will only give you two repos. This is useful if you want to put it in
264 | your `.bashrc`, `.zshrc`, or `fish_greeting` function.
265 |
266 | Just add `starcli -r 3 -L grid` in there, and every time you open your terminal,
267 | you will find 3 trending repos printed neatly in a grid format, great way to
268 | start your day (a bit like the [Hacker Tab
269 | Extension](https://chrome.google.com/webstore/detail/hacker-tab/ibomigipadcieapbemkegkmadbbanbgm?hl=en)).
270 |
271 |
272 | ### Paging
273 |
274 | Result output can be displayed through your OS pager using the `--pager`/`-p`
275 | flag.
276 |
277 | If you're using less, add `R` to your `LESS` environment variable so colors and
278 | styling can be displayed correctly.
279 |
280 |
281 | ### GitHub Authentication
282 |
283 | Rate limit may be hit if starcli sends many repeated requests to GitHub within a
284 | short perod of time.
285 |
286 | To avoid this, provide an authentication token using `--auth`:
287 |
288 | ```
289 | starcli --auth 'username:token'
290 | ```
291 |
292 | [Read more about authentication tokens on GitHub
293 | Docs](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token")
294 |
295 |
296 | ## Issues, feature request, and feedback
297 |
298 | * Issues, bug reports, or feature request: Don't hesitate to open an issue in
299 | this repo
300 | * Feedback: any general feedback or questions about using StarCLI you can leave
301 | a comment on the [Product Hunt
302 | page](https://www.producthunt.com/posts/starcli)
303 |
304 |
305 | ## Development [](http://makeapullrequest.com)
306 |
307 | For contributing guidelines and how to set up your development environment,
308 | please read
309 | [`CONTRIBUTING.md`](https://github.com/hedyhli/starcli/blob/main/CONTRIBUTING.md).
310 | Remember that all contributions to this project should follow its [CODE OF
311 | CONDUCT](https://github.com/hedyhli/starcli/blob/main/CODE_OF_CONDUCT.md).
312 |
313 |
314 | ## Uses
315 |
316 | * CommandLine Argument parser: [Click](https://github.com/pallets/click)
317 | * Colored and table console print: [`rich`](https://github.com/willmcgugan/rich)
318 | (with click and colorama)
319 | * HTTP library to send requests: [`requests`](https://github.com/psf/requests)
320 |
321 |
322 |
323 | ## Contributors ✨
324 |
325 | Thanks goes to all of these wonderful people ([emoji
326 | key](https://allcontributors.org/docs/en/emoji-key)):
327 |
328 |
329 |
330 |
331 |
358 |
359 |
360 |
361 |
362 |
363 |
364 | This project follows the
365 | [all-contributors](https://github.com/all-contributors/all-contributors)
366 | specification. Contributions of any kind welcome!
367 |
368 |
369 | ## Credits
370 |
371 | This project was forked from [`githunt`
372 | (python)](https://github.com/SriNandan33/githunt) and its initial intention was
373 | to rewrite that project to use Rich instead of colorama + tabulate, but now it
374 | has so much more features than before, thanks to everyone's contributions 🙌
375 |
--------------------------------------------------------------------------------
/assets/daterange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hedyhli/starcli/d2d3b20bf04a6176d141c463ffb15b25caa60ad2/assets/daterange.png
--------------------------------------------------------------------------------
/assets/grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hedyhli/starcli/d2d3b20bf04a6176d141c463ffb15b25caa60ad2/assets/grid.png
--------------------------------------------------------------------------------
/assets/lang.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hedyhli/starcli/d2d3b20bf04a6176d141c463ffb15b25caa60ad2/assets/lang.png
--------------------------------------------------------------------------------
/assets/list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hedyhli/starcli/d2d3b20bf04a6176d141c463ffb15b25caa60ad2/assets/list.png
--------------------------------------------------------------------------------
/assets/starcli-demo2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hedyhli/starcli/d2d3b20bf04a6176d141c463ffb15b25caa60ad2/assets/starcli-demo2.gif
--------------------------------------------------------------------------------
/assets/starcli-small-cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hedyhli/starcli/d2d3b20bf04a6176d141c463ffb15b25caa60ad2/assets/starcli-small-cover.png
--------------------------------------------------------------------------------
/assets/starcli_cover.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hedyhli/starcli/d2d3b20bf04a6176d141c463ffb15b25caa60ad2/assets/starcli_cover.PNG
--------------------------------------------------------------------------------
/assets/table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hedyhli/starcli/d2d3b20bf04a6176d141c463ffb15b25caa60ad2/assets/table.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Click~=7.0
2 | gtrending>=0.3.0,<1.0.0
3 | requests>=2.22.0,<3.0.0
4 | rich>=4.0.0,<14.0.0
5 | xdg>=5.1.1,<6.0.0
6 |
--------------------------------------------------------------------------------
/requirements_dev.txt:
--------------------------------------------------------------------------------
1 | -r requirements.txt
2 | black>=19.10b0
3 | codespell>=1.17.1
4 | pylint>=2.4.4
5 | pytest>=5.4.3
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """ setup """
2 |
3 | import io
4 |
5 | from setuptools import setup
6 |
7 | with io.open("README.md", "rt", encoding="utf8") as f:
8 | LONG_DESC = f.read()
9 |
10 | VERSION = "2.18.1"
11 |
12 | # This call to setup() does all the work
13 | setup(
14 | name="starcli",
15 | version=VERSION,
16 | description="Browse popular projects on github by star trends from your command line",
17 | long_description=LONG_DESC,
18 | long_description_content_type="text/markdown",
19 | python_requires=">=3.6",
20 | url="https://github.com/hedyhli/starcli",
21 | author="hedy",
22 | author_email="hedy@tilde.cafe",
23 | license="MIT",
24 | classifiers=[
25 | "License :: OSI Approved :: MIT License",
26 | "Programming Language :: Python :: 3",
27 | "Programming Language :: Python :: 3.7",
28 | "Programming Language :: Python :: 3.8",
29 | "Programming Language :: Python :: 3.9",
30 | "Programming Language :: Python :: 3.10",
31 | "Programming Language :: Python :: 3.11",
32 | ],
33 | packages=["starcli"],
34 | include_package_data=True,
35 | install_requires=[
36 | "Click>=7.0,<8.0",
37 | "gtrending>=0.3.0,<1.0.0",
38 | "requests>=2.22.0,<3.0.0",
39 | "rich>=4.0.0,<14.0.0",
40 | "xdg>=5.1.1,<6.0.0",
41 | ],
42 | entry_points={
43 | "console_scripts": [
44 | "starcli=starcli.__main__:cli",
45 | ]
46 | },
47 | )
48 |
--------------------------------------------------------------------------------
/starcli/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hedyhli/starcli/d2d3b20bf04a6176d141c463ffb15b25caa60ad2/starcli/__init__.py
--------------------------------------------------------------------------------
/starcli/__main__.py:
--------------------------------------------------------------------------------
1 | """starcli.__main__
2 |
3 | The main CLI module of starcli.
4 | """
5 |
6 | import re
7 | import json
8 | import os
9 | from datetime import datetime, timedelta
10 |
11 | import click
12 | from xdg import XDG_CACHE_HOME
13 |
14 | from .layouts import print_results, shorten_count
15 | from .search import (
16 | search,
17 | debug_requests_on,
18 | search_github_trending,
19 | # search_error,
20 | # status_actions,
21 | )
22 |
23 |
24 | # could be made into config option in the future
25 | CACHE_DIR = XDG_CACHE_HOME / "starcli"
26 | CACHED_RESULT_PATH = CACHE_DIR / "results.json"
27 | CACHED_RESULT_PATH_LEGACY = XDG_CACHE_HOME / "starcli.json"
28 | CACHE_EXPIRATION = 1 # Minutes
29 |
30 |
31 | @click.command()
32 | @click.option(
33 | "--lang",
34 | "-l",
35 | multiple=True,
36 | type=str,
37 | default=[""],
38 | help="Language filter eg: python. (can be used multiple times)",
39 | )
40 | @click.option(
41 | "--spoken-language",
42 | "-S",
43 | type=str,
44 | default="",
45 | help="Spoken Language filter eg: en for English, zh for Chinese",
46 | )
47 | @click.option(
48 | "--created",
49 | "-c",
50 | default="",
51 | help="Specify repo creation date in YYYY-MM-DD, use >date, <=date etc to be more specific.",
52 | )
53 | @click.option(
54 | "--topic",
55 | "-t",
56 | default=[],
57 | multiple=True,
58 | help="Search by repo topics. Can be used multiple times.",
59 | )
60 | @click.option(
61 | "--pushed",
62 | "-p",
63 | default="",
64 | help="Date of last push in YYYY-MM-DD (>, <, >=, <= specifiers supported)",
65 | )
66 | @click.option(
67 | "--layout",
68 | "-L",
69 | type=click.Choice(["list", "table", "grid"], case_sensitive=False),
70 | help="The output format (list, table, or grid), default is list",
71 | )
72 | @click.option(
73 | "--stars",
74 | "-s",
75 | type=str,
76 | default=">=100",
77 | help="Number of stars, default is '>=100'. eg: '>0', '123', '<50000",
78 | )
79 | @click.option(
80 | "--num-results",
81 | "-n",
82 | type=int,
83 | default=7,
84 | help="The number of items in the results. Default: 7",
85 | )
86 | @click.option(
87 | "--order",
88 | "-o",
89 | type=click.Choice(["desc", "asc"], case_sensitive=False),
90 | default="desc",
91 | help="Order of repos by stars, 'desc' or 'asc', default: desc",
92 | )
93 | @click.option(
94 | "--long-stats",
95 | is_flag=True,
96 | help="Print the actual stats number (1300 instead of 1.3k)",
97 | )
98 | @click.option(
99 | "--date-range",
100 | "-d",
101 | type=click.Choice(["day", "week", "month"], case_sensitive=False),
102 | help="View stars received within time, choose from: day, week, month. Uses GitHub trending for fetching results, hence some other filter options may not work.",
103 | )
104 | @click.option(
105 | "--user",
106 | "-u",
107 | type=str,
108 | default="",
109 | help="Filter for trending repositories by username",
110 | )
111 | @click.option(
112 | "--auth",
113 | type=str,
114 | default="",
115 | help="Optionally use GitHub personal access token in the format 'username:password'.",
116 | )
117 | @click.option(
118 | "--pager",
119 | "-P",
120 | is_flag=True,
121 | default=False,
122 | help="Use $PAGER to page output. (put -r in $LESS to enable ANSI styles)",
123 | )
124 | @click.option( # Used for debugging CLI input, prevent hitting GitHub API when we don't need it
125 | "--nop",
126 | is_flag=True,
127 | default=False,
128 | hidden=True,
129 | )
130 | @click.option("--debug", is_flag=True, default=False, help="Turn on debugging mode")
131 | def cli(
132 | lang,
133 | spoken_language,
134 | created,
135 | topic,
136 | pushed,
137 | layout,
138 | stars,
139 | num_results,
140 | order,
141 | long_stats,
142 | date_range,
143 | user,
144 | debug=False,
145 | auth="",
146 | pager=False,
147 | nop=False,
148 | ):
149 | """Find trending repos on GitHub"""
150 | if debug:
151 | import logging
152 |
153 | debug_requests_on()
154 |
155 | tmp_repos = None
156 | options_key = "{lang}_{spoken_language}_{created}_{topic}_{pushed}_{stars}_{order}_{date_range}_{user}".format(
157 | lang=lang,
158 | spoken_language=spoken_language,
159 | created=created,
160 | topic=topic,
161 | pushed=pushed,
162 | stars=stars,
163 | order=order,
164 | date_range=date_range,
165 | user=user,
166 | )
167 |
168 | if os.path.exists(CACHED_RESULT_PATH):
169 | with open(CACHED_RESULT_PATH, "r") as f:
170 | json_file = json.load(f)
171 | result = json_file.get(options_key)
172 | if result:
173 | t = result[-1].get("time")
174 | time = datetime.strptime(t, "%Y-%m-%d %H:%M:%S.%f")
175 | diff = datetime.now() - time
176 | if diff < timedelta(minutes=CACHE_EXPIRATION):
177 | if debug:
178 | logger = logging.getLogger(__name__)
179 | logger.debug("Fetching results from cache")
180 |
181 | tmp_repos = result
182 |
183 | if not tmp_repos: # If cache expired or results not yet cached
184 | if auth and not re.search(".+:.+", auth): # Check authentication format
185 | click.secho(
186 | f"Invalid authentication format: {auth}. Must be 'username:token'",
187 | fg="bright_red",
188 | )
189 | click.secho(
190 | "Use --help or see: "
191 | "https://docs.github.com/en/github/authenticating-to-github/"
192 | "creating-a-personal-access-token",
193 | fg="bright_red",
194 | )
195 | auth = None
196 |
197 | if nop:
198 | return
199 |
200 | if (
201 | not spoken_language and not date_range
202 | ): # if filtering by spoken language and date range not required
203 | tmp_repos = search(
204 | lang, created, pushed, stars, topic, user, debug, order, auth
205 | )
206 | else:
207 | tmp_repos = search_github_trending(
208 | lang, spoken_language, order, stars, date_range, debug
209 | )
210 |
211 | if not tmp_repos: # if search() returned None
212 | return
213 | else: # Cache results
214 | tmp_repos.append({"time": str(datetime.now())})
215 | # make sure ~/.cache dir exists
216 | os.makedirs(CACHE_DIR, exist_ok=True)
217 | # Clean legacy cache file path
218 | try:
219 | os.remove(CACHED_RESULT_PATH_LEGACY)
220 | except FileNotFoundError:
221 | pass
222 |
223 | with open(CACHED_RESULT_PATH, "a+") as f:
224 | if os.path.getsize(CACHED_RESULT_PATH) == 0: # file is empty
225 | result_dict = {options_key: tmp_repos}
226 | f.write(json.dumps(result_dict, indent=4))
227 | else: # file is not empty
228 | f.seek(0)
229 | result_dict = json.load(f)
230 | result_dict[options_key] = tmp_repos
231 | f.truncate(0)
232 | f.write(json.dumps(result_dict, indent=4))
233 |
234 | repos = tmp_repos[0:num_results]
235 |
236 | if not long_stats: # shorten the stat counts when not using --long-stats
237 | for repo in repos:
238 | repo["stargazers_count"] = shorten_count(repo["stargazers_count"])
239 | repo["forks"] = shorten_count(repo["forks"])
240 | if "date_range" in repo.keys() and repo["date_range"]:
241 | num_stars = repo["date_range"].split()[0]
242 | repo["date_range"] = repo["date_range"].replace(
243 | num_stars, str(shorten_count(int(num_stars.replace(",", ""))))
244 | )
245 |
246 | print_results(repos, page=pager, layout=layout)
247 |
248 |
249 | if __name__ == "__main__":
250 | # pylint: disable=no-value-for-parameter
251 | cli()
252 |
--------------------------------------------------------------------------------
/starcli/layouts.py:
--------------------------------------------------------------------------------
1 | """starcli.layouts"""
2 |
3 | import math
4 |
5 | from rich.align import Align
6 | from rich.console import Console, group
7 | from rich.rule import Rule
8 | from rich.table import Table
9 | from rich.text import Text
10 | from rich.panel import Panel
11 | from rich.columns import Columns
12 |
13 |
14 | console = Console()
15 |
16 | SYMBOL_MAP = {"stars": "★", "forks": "⎇"}
17 |
18 |
19 | def shorten_count(number):
20 | """Shortens number"""
21 | if number < 1000:
22 | return str(number)
23 |
24 | number = int(number)
25 | new_number = math.ceil(round(number / 100.0, 1)) * 100
26 |
27 | if new_number % 1000 == 0:
28 | return str(new_number)[0] + "k"
29 | if new_number < 1000:
30 | # returns the same old integer if no changes were made
31 | return str(number)
32 | # returns a new string if the number was shortened
33 | return str(new_number / 1000.0) + "k"
34 |
35 |
36 | def format_stats(stars, forks):
37 | """Formatted string of repo stats"""
38 | stats = f"{stars}{SYMBOL_MAP['stars']} " if stars != "-1" else ""
39 | stats += f"{forks}{SYMBOL_MAP['forks']} " if forks != "-1" else ""
40 | return stats
41 |
42 |
43 | def format_date_range(date_range):
44 | """Formatted and styled Text object of date_range period stars"""
45 | if not date_range:
46 | return Text("")
47 | return (
48 | Text("(", style="reset")
49 | .append(
50 | (date_range.replace(" stars", SYMBOL_MAP["stars"])), style="italic magenta"
51 | )
52 | .append(")")
53 | )
54 |
55 |
56 | def list_layout(repos):
57 | """Display repositories in a list layout using rich"""
58 |
59 | width = 80
60 |
61 | @group()
62 | def render_repo(repo):
63 | """Yields renderables for a single repo."""
64 | yield Rule(style="bright_yellow")
65 | yield ""
66 | # Table with description and stats
67 | title_table = Table.grid(padding=(0, 1))
68 | title_table.expand = True
69 | title = Text(repo["full_name"], overflow="fold")
70 | title.stylize(f"yellow link {repo['html_url']}")
71 |
72 | stats = format_stats(repo["stargazers_count"], repo["forks"])
73 | date_range_col = format_date_range(repo.get("date_range"))
74 |
75 | title_table.add_row(title, Text(stats, style="italic blue"))
76 | title_table.columns[1].no_wrap = True
77 | title_table.columns[1].justify = "right"
78 | yield title_table
79 | yield ""
80 | lang_table = Table.grid(padding=(0, 1))
81 | lang_table.expand = True
82 | language_col = (
83 | Text(repo["language"], style="bold cyan")
84 | if repo["language"]
85 | else Text("no language")
86 | )
87 | lang_table.add_row(language_col, date_range_col)
88 | lang_table.columns[1].justify = "right"
89 | yield lang_table
90 | yield ""
91 | # Description
92 | description = repo["description"]
93 | if description:
94 | yield Text(description.strip())
95 | else:
96 | yield "[i]no description"
97 | yield ""
98 |
99 | def column(renderable):
100 | """Constrain width and align to center to create a column."""
101 | return Align.center(renderable, width=width, pad=False)
102 |
103 | for repo in repos:
104 | console.print(column(render_repo(repo)))
105 | console.print(column(Rule(style="bright_yellow")))
106 |
107 |
108 | def table_layout(repos):
109 | """Displays repositories in a table format using rich"""
110 | table = Table(leading=1)
111 |
112 | # make the columns
113 | table.add_column("Name", style="bold yellow")
114 | table.add_column("Language")
115 | table.add_column("Description")
116 | table.add_column("Stats", justify="right")
117 |
118 | for repo in repos:
119 | stats = Text(
120 | format_stats(repo["stargazers_count"], repo["forks"]), style="blue"
121 | )
122 | stats.append("\n").append(format_date_range(repo.get("date_range")))
123 |
124 | language = (
125 | Text(repo["language"], style="cyan")
126 | if repo["language"]
127 | else Text("no language", style="italic")
128 | )
129 | description = (
130 | Text(repo["description"])
131 | if repo["description"]
132 | else Text("no description", style="italic")
133 | )
134 |
135 | name = Text(repo["name"], overflow="fold")
136 | name.stylize(f"yellow link {repo['html_url']}")
137 |
138 | table.add_row(name, language, description, stats)
139 |
140 | console.print(table)
141 |
142 |
143 | def grid_layout(repos):
144 | """Displays repositories in a grid format using rich"""
145 |
146 | max_desc_len = 90
147 |
148 | panels = []
149 | for repo in repos:
150 |
151 | stats = format_stats(repo["stargazers_count"], repo["forks"])
152 | # '\n' added here as it would group both text and new line together
153 | # hence if date_range isn't present the new line will also not be displayed
154 | date_range = format_date_range(repo.get("date_range")).append("\n")
155 |
156 | language = (
157 | Text(repo["language"], style="cyan")
158 | if repo["language"]
159 | else Text("no language", style="italic")
160 | )
161 | description = (
162 | Text(repo["description"])
163 | if repo["description"]
164 | else Text("no description", style="italic")
165 | )
166 |
167 | name = Text(repo["name"], style="bold yellow")
168 | name.stylize(f"link {repo['html_url']}")
169 | stats = Text(stats, style="blue")
170 |
171 | # truncate rest of the description if
172 | # it's more than 90 (max_desc_len) chars
173 | # using truncate() is better than textwrap
174 | # because it also takes care of asian characters
175 | description.truncate(max_desc_len, overflow="ellipsis")
176 |
177 | repo_summary = Text.assemble(
178 | name,
179 | "\n",
180 | stats,
181 | " ",
182 | date_range,
183 | language,
184 | "\n",
185 | description,
186 | )
187 | panels.append(Panel(repo_summary, expand=True))
188 |
189 | console.print((Columns(panels, width=30, expand=True)))
190 |
191 |
192 | def print_results(*args, page=False, layout=""):
193 | """Use a specified layout to print or page the fetched results"""
194 | if page:
195 | with console.pager():
196 | print_layout(layout=layout, *args)
197 | return
198 | print_layout(
199 | layout=layout,
200 | *args,
201 | )
202 |
203 |
204 | def print_layout(*args, layout="list"):
205 | """Use specified layout"""
206 | if layout == "table":
207 | table_layout(*args)
208 | elif layout == "grid":
209 | grid_layout(*args)
210 | else:
211 | list_layout(*args)
212 |
--------------------------------------------------------------------------------
/starcli/search.py:
--------------------------------------------------------------------------------
1 | """starcli.search
2 |
3 | The search module responsible for handling requests and querying GitHub
4 | """
5 |
6 | # Standard library imports
7 | from datetime import datetime, timedelta
8 | from time import sleep
9 | import logging
10 | from random import randint
11 | import re
12 | import http.client
13 | import typing as t
14 |
15 | # Third party imports
16 | import requests
17 | from click import secho
18 | import gtrending
19 | from rich.logging import RichHandler
20 |
21 | API_URL = "https://api.github.com/search/repositories"
22 |
23 | # "X stars today" / "X stars this week"
24 | # today, this week, this month must be first item in the tuple for use in date_range_str()
25 | DATE_RANGE_MAP = {
26 | "daily": ("today", "day"),
27 | "weekly": ("this week", "week"),
28 | "monthly": ("this month", "month"),
29 | }
30 |
31 | STATUS_ACTIONS = {
32 | "retry": "Failed to retrieve data. Retrying in ",
33 | "invalid": "The server was unable to process the request.",
34 | "unauthorized": "The server did not accept the credentials.\n" \
35 | "See: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token\n" #
36 | "Maybe you did not give enough scopes?",
37 | "not_found": "The server indicated no data was found.",
38 | "unsupported": "The request is not supported.",
39 | "unknown": "An unknown error occurred.",
40 | "valid": "The request returned successfully, but an unknown exception occurred.",
41 | }
42 |
43 | FORMAT = "%(message)s"
44 |
45 | httpclient_logger = logging.getLogger("http.client")
46 |
47 |
48 | def httpclient_logging_debug(level=logging.DEBUG):
49 | def httpclient_log(*args):
50 | httpclient_logger.log(level, " ".join(args))
51 |
52 | http.client.print = httpclient_log
53 | http.client.HTTPConnection.debuglevel = 1
54 |
55 |
56 | def debug_requests_on():
57 | """Turn on the logging for requests"""
58 | logging.basicConfig(
59 | level=logging.DEBUG,
60 | format=FORMAT,
61 | datefmt="[%Y-%m-%d]",
62 | handlers=[RichHandler()],
63 | )
64 | logger = logging.getLogger(__name__)
65 |
66 | from http.client import HTTPConnection
67 |
68 | httpclient_logging_debug()
69 |
70 | requests_log = logging.getLogger("requests.packages.urllib3")
71 | requests_log.setLevel(logging.DEBUG)
72 | requests_log.propagate = True
73 |
74 |
75 | def debug_logger(debug: bool, *args, **kwargs):
76 | """Log a message if debug mode is on"""
77 | if debug:
78 | debug_requests_on()
79 | logger = logging.getLogger(__name__)
80 | logger.debug(*args, **kwargs)
81 |
82 |
83 | def convert_datetime(date: str, date_format: str = "%Y-%m-%d"):
84 | """Safely convert a date string to datetime"""
85 | try:
86 | # try to turn the string into a date-time object
87 | tmp_date = datetime.strptime(date, date_format)
88 | except ValueError: # ValueError will be thrown if format is invalid
89 | secho(
90 | f"Invalid date format: {date}. Must be yyyy-mm-dd",
91 | fg="bright_red",
92 | )
93 | return None
94 | return tmp_date
95 |
96 |
97 | def get_date(date):
98 | """Find the date info in a string"""
99 | prefix = ""
100 | if any(i in date[0] for i in [">", "=", "<"]):
101 | if "=" in date[1]:
102 | prefix = date[:2]
103 | date = date.strip(prefix)
104 | else:
105 | prefix = date[0]
106 | date = date.strip(prefix)
107 | tmp_date = convert_datetime(date)
108 | if not tmp_date:
109 | return None
110 | return prefix + tmp_date.strftime("%Y-%m-%d")
111 |
112 |
113 | def get_valid_request(
114 | url: str, auth: t.Optional[str] = ""
115 | ) -> t.Optional[requests.Response]:
116 | """GET an url with auth and handle a connection error"""
117 | while True:
118 | try:
119 | session = requests.Session()
120 | if auth:
121 | session.auth = (auth.split(":")[0], auth.split(":")[1])
122 | request = session.get(url)
123 | except requests.exceptions.ConnectionError:
124 | secho("Internet connection error...", fg="bright_red")
125 | return
126 |
127 | if not request.status_code in (200, 202):
128 | handling_code = search_error(request.status_code)
129 | if handling_code == "retry":
130 | for i in range(15, 0, -1):
131 | secho(
132 | f"{STATUS_ACTIONS[handling_code]} {i} seconds...",
133 | fg="bright_yellow",
134 | ) # Print and update a timer
135 |
136 | sleep(1)
137 | elif handling_code in STATUS_ACTIONS:
138 | secho(STATUS_ACTIONS[handling_code], fg="bright_yellow")
139 | return
140 | else:
141 | secho("An invalid handling code was returned.", fg="bright_red")
142 | return
143 | else:
144 | break
145 |
146 | return request
147 |
148 |
149 | def search_error(status_code: t.Union[int, str]):
150 | """Get a directive on how to handle a given HTTP status code"""
151 | int_status_code = int(status_code) # Make sure the status code is an integer
152 |
153 | http_code_handling = {
154 | "200": "valid",
155 | "202": "valid",
156 | "204": "valid",
157 | "400": "invalid",
158 | "401": "unauthorized",
159 | "403": "retry",
160 | "404": "not_found",
161 | "405": "invalid",
162 | "422": "not_found",
163 | "500": "invalid",
164 | "501": "invalid",
165 | }
166 |
167 | try:
168 | return http_code_handling[str(int_status_code)]
169 | except KeyError:
170 | return "unsupported"
171 |
172 |
173 | def convert_date_range(param: str) -> t.Optional[str]:
174 | """Convert the date range parameter into 'since' parameter for GitHub Trending"""
175 | if not param:
176 | return None
177 | param = param.lower().replace(" ", "-")
178 | for key, aliases in DATE_RANGE_MAP.items():
179 | if param == key or param in aliases:
180 | return key
181 | raise ValueError(f"Invalid date range parameter: {param}")
182 |
183 |
184 | def date_range_str(period_stars: int, since: int) -> str:
185 | """Generate a readable string to display the current date range stars increase
186 |
187 | Parameters:
188 | since (int): The argument used for GitHub Trending search.
189 |
190 | Returns:
191 | str: The formatted string for output.
192 | """
193 | date_range = DATE_RANGE_MAP.get(since, (None,))[0]
194 | return f"+{period_stars} stars {date_range}"
195 |
196 |
197 | def search(
198 | languages=[""],
199 | created=None,
200 | pushed=None,
201 | stars=">=100",
202 | topics=[],
203 | user=None,
204 | debug=False,
205 | order="desc",
206 | auth="",
207 | ):
208 | """Return repositories searched from GitHub API"""
209 | date_format = "%Y-%m-%d" # date format in iso format
210 | debug_logger(debug, f"Search: created param: {created}")
211 | debug_logger(debug, f"Search: order param: {order}")
212 |
213 | day_range = 0 - randint(100, 400) # random negative from 100 to 400
214 |
215 | if not created: # if created not provided
216 | # creation date: the time now minus a random number of days
217 | # 100 to 400 days - which was stored in day_range
218 | created_str = ">=" + (datetime.utcnow() + timedelta(days=day_range)).strftime(
219 | date_format
220 | )
221 | else: # if created is provided
222 | created_str = get_date(created)
223 | if not created_str:
224 | return None
225 |
226 | if not pushed: # if pushed not provided
227 | # pushed date: start, is the time now minus a random number of days
228 | # 100 to 400 days - which was stored in day_range
229 | pushed_str = ">=" + (datetime.utcnow() + timedelta(days=day_range)).strftime(
230 | date_format
231 | )
232 | else: # if pushed is provided
233 | pushed_str = get_date(pushed)
234 | if not pushed_str:
235 | return None
236 |
237 | query = ""
238 | if user:
239 | query = f"user:{user}+"
240 |
241 | query += f"stars:{stars}+created:{created_str}" # construct query
242 | query += f"+pushed:{pushed_str}" # add pushed info to query
243 | query += "".join(
244 | [f"+language:{language}" for language in languages]
245 | ) # add language to query
246 | query += "".join(["+topic:" + i for i in topics]) # add topics to query
247 |
248 | url = f"{API_URL}?q={query}&sort=stars&order={order}" # use query to construct url
249 | debug_logger(debug, f"Search: url: {url}")
250 | if auth:
251 | debug_logger(debug, "Auth: on")
252 | else:
253 | debug_logger(debug, "Auth: off")
254 |
255 | request = get_valid_request(url, auth)
256 | if request is None:
257 | return request
258 |
259 | return request.json()["items"]
260 |
261 |
262 | def search_github_trending(
263 | languages=[""],
264 | spoken_language=None,
265 | order="desc",
266 | stars=">=10",
267 | date_range=None,
268 | debug=False,
269 | ) -> t.List[dict]:
270 | """Returns trending repositories from github trending page"""
271 | gtrending_repo_list = []
272 | since = convert_date_range(date_range)
273 |
274 | debug_logger(debug, f"gtrending: since: {since}")
275 |
276 | for language in languages:
277 | if date_range:
278 | debug_logger(
279 | debug,
280 | f"gtrending: fetching repos: language={language}, spoken_language={spoken_language}, since={since}",
281 | )
282 | gtrending_repo_list += gtrending.fetch_repos(
283 | language, spoken_language, since
284 | )
285 | else:
286 | debug_logger(
287 | debug,
288 | f"gtrending: fetching repos: language={language}, spoken_language={spoken_language}",
289 | )
290 | gtrending_repo_list += gtrending.fetch_repos(language, spoken_language)
291 |
292 | repositories = []
293 | for gtrending_repo in gtrending_repo_list:
294 | repo_dict = convert_repo_dict(gtrending_repo)
295 | repo_dict["date_range"] = (
296 | date_range_str(repo_dict["date_range"], since) if date_range else None
297 | )
298 | # filter by number of stars
299 | num = [int(s) for s in re.findall(r"\d+", stars)][0]
300 | if (
301 | ("<" in stars and repo_dict["stargazers_count"] < num)
302 | or ("<=" in stars and repo_dict["stargazers_count"] <= num)
303 | or (">" in stars and repo_dict["stargazers_count"] > num)
304 | or (">=" in stars and repo_dict["stargazers_count"] >= num)
305 | ):
306 | repositories.append(repo_dict)
307 |
308 | if order == "asc":
309 | return sorted(repositories, key=lambda repo: repo["stargazers_count"])
310 | return sorted(repositories, key=lambda repo: repo["stargazers_count"], reverse=True)
311 |
312 |
313 | def convert_repo_dict(gtrending_repo: dict) -> dict:
314 | """Normalize dictionary keys returned by gtrending"""
315 | repo_dict = {}
316 | repo_dict["full_name"] = gtrending_repo.get("fullname")
317 | repo_dict["name"] = gtrending_repo.get("name")
318 | repo_dict["html_url"] = gtrending_repo.get("url")
319 | repo_dict["stargazers_count"] = gtrending_repo.get("stars", -1)
320 | repo_dict["forks"] = gtrending_repo.get("forks", -1)
321 | repo_dict["language"] = gtrending_repo.get("language")
322 | # gtrending_repo has key `description` and value is empty string if it's empty
323 | repo_dict["description"] = (
324 | gtrending_repo.get("description")
325 | if gtrending_repo.get("description") != ""
326 | else None
327 | )
328 | repo_dict["date_range"] = gtrending_repo.get("currentPeriodStars")
329 | return repo_dict
330 |
--------------------------------------------------------------------------------
/starcli/spoken-languages.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "urlParam": "ab",
4 | "name": "Abkhazian"
5 | },
6 | {
7 | "urlParam": "aa",
8 | "name": "Afar"
9 | },
10 | {
11 | "urlParam": "af",
12 | "name": "Afrikaans"
13 | },
14 | {
15 | "urlParam": "ak",
16 | "name": "Akan"
17 | },
18 | {
19 | "urlParam": "sq",
20 | "name": "Albanian"
21 | },
22 | {
23 | "urlParam": "am",
24 | "name": "Amharic"
25 | },
26 | {
27 | "urlParam": "ar",
28 | "name": "Arabic"
29 | },
30 | {
31 | "urlParam": "an",
32 | "name": "Aragonese"
33 | },
34 | {
35 | "urlParam": "hy",
36 | "name": "Armenian"
37 | },
38 | {
39 | "urlParam": "as",
40 | "name": "Assamese"
41 | },
42 | {
43 | "urlParam": "av",
44 | "name": "Avaric"
45 | },
46 | {
47 | "urlParam": "ae",
48 | "name": "Avestan"
49 | },
50 | {
51 | "urlParam": "ay",
52 | "name": "Aymara"
53 | },
54 | {
55 | "urlParam": "az",
56 | "name": "Azerbaijani"
57 | },
58 | {
59 | "urlParam": "bm",
60 | "name": "Bambara"
61 | },
62 | {
63 | "urlParam": "ba",
64 | "name": "Bashkir"
65 | },
66 | {
67 | "urlParam": "eu",
68 | "name": "Basque"
69 | },
70 | {
71 | "urlParam": "be",
72 | "name": "Belarusian"
73 | },
74 | {
75 | "urlParam": "bn",
76 | "name": "Bengali"
77 | },
78 | {
79 | "urlParam": "bh",
80 | "name": "Bihari languages"
81 | },
82 | {
83 | "urlParam": "bi",
84 | "name": "Bislama"
85 | },
86 | {
87 | "urlParam": "bs",
88 | "name": "Bosnian"
89 | },
90 | {
91 | "urlParam": "br",
92 | "name": "Breton"
93 | },
94 | {
95 | "urlParam": "bg",
96 | "name": "Bulgarian"
97 | },
98 | {
99 | "urlParam": "my",
100 | "name": "Burmese"
101 | },
102 | {
103 | "urlParam": "ca",
104 | "name": "Catalan, Valencian"
105 | },
106 | {
107 | "urlParam": "ch",
108 | "name": "Chamorro"
109 | },
110 | {
111 | "urlParam": "ce",
112 | "name": "Chechen"
113 | },
114 | {
115 | "urlParam": "ny",
116 | "name": "Chichewa, Chewa, Nyanja"
117 | },
118 | {
119 | "urlParam": "zh",
120 | "name": "Chinese"
121 | },
122 | {
123 | "urlParam": "cv",
124 | "name": "Chuvash"
125 | },
126 | {
127 | "urlParam": "kw",
128 | "name": "Cornish"
129 | },
130 | {
131 | "urlParam": "co",
132 | "name": "Corsican"
133 | },
134 | {
135 | "urlParam": "cr",
136 | "name": "Cree"
137 | },
138 | {
139 | "urlParam": "hr",
140 | "name": "Croatian"
141 | },
142 | {
143 | "urlParam": "cs",
144 | "name": "Czech"
145 | },
146 | {
147 | "urlParam": "da",
148 | "name": "Danish"
149 | },
150 | {
151 | "urlParam": "dv",
152 | "name": "Divehi, Dhivehi, Maldivian"
153 | },
154 | {
155 | "urlParam": "nl",
156 | "name": "Dutch, Flemish"
157 | },
158 | {
159 | "urlParam": "dz",
160 | "name": "Dzongkha"
161 | },
162 | {
163 | "urlParam": "en",
164 | "name": "English"
165 | },
166 | {
167 | "urlParam": "eo",
168 | "name": "Esperanto"
169 | },
170 | {
171 | "urlParam": "et",
172 | "name": "Estonian"
173 | },
174 | {
175 | "urlParam": "ee",
176 | "name": "Ewe"
177 | },
178 | {
179 | "urlParam": "fo",
180 | "name": "Faroese"
181 | },
182 | {
183 | "urlParam": "fj",
184 | "name": "Fijian"
185 | },
186 | {
187 | "urlParam": "fi",
188 | "name": "Finnish"
189 | },
190 | {
191 | "urlParam": "fr",
192 | "name": "French"
193 | },
194 | {
195 | "urlParam": "ff",
196 | "name": "Fulah"
197 | },
198 | {
199 | "urlParam": "gl",
200 | "name": "Galician"
201 | },
202 | {
203 | "urlParam": "ka",
204 | "name": "Georgian"
205 | },
206 | {
207 | "urlParam": "de",
208 | "name": "German"
209 | },
210 | {
211 | "urlParam": "el",
212 | "name": "Greek, Modern"
213 | },
214 | {
215 | "urlParam": "gn",
216 | "name": "Guarani"
217 | },
218 | {
219 | "urlParam": "gu",
220 | "name": "Gujarati"
221 | },
222 | {
223 | "urlParam": "ht",
224 | "name": "Haitian, Haitian Creole"
225 | },
226 | {
227 | "urlParam": "ha",
228 | "name": "Hausa"
229 | },
230 | {
231 | "urlParam": "he",
232 | "name": "Hebrew"
233 | },
234 | {
235 | "urlParam": "hz",
236 | "name": "Herero"
237 | },
238 | {
239 | "urlParam": "hi",
240 | "name": "Hindi"
241 | },
242 | {
243 | "urlParam": "ho",
244 | "name": "Hiri Motu"
245 | },
246 | {
247 | "urlParam": "hu",
248 | "name": "Hungarian"
249 | },
250 | {
251 | "urlParam": "ia",
252 | "name": "Interlingua (International Auxil..."
253 | },
254 | {
255 | "urlParam": "id",
256 | "name": "Indonesian"
257 | },
258 | {
259 | "urlParam": "ie",
260 | "name": "Interlingue, Occidental"
261 | },
262 | {
263 | "urlParam": "ga",
264 | "name": "Irish"
265 | },
266 | {
267 | "urlParam": "ig",
268 | "name": "Igbo"
269 | },
270 | {
271 | "urlParam": "ik",
272 | "name": "Inupiaq"
273 | },
274 | {
275 | "urlParam": "io",
276 | "name": "Ido"
277 | },
278 | {
279 | "urlParam": "is",
280 | "name": "Icelandic"
281 | },
282 | {
283 | "urlParam": "it",
284 | "name": "Italian"
285 | },
286 | {
287 | "urlParam": "iu",
288 | "name": "Inuktitut"
289 | },
290 | {
291 | "urlParam": "ja",
292 | "name": "Japanese"
293 | },
294 | {
295 | "urlParam": "jv",
296 | "name": "Javanese"
297 | },
298 | {
299 | "urlParam": "kl",
300 | "name": "Kalaallisut, Greenlandic"
301 | },
302 | {
303 | "urlParam": "kn",
304 | "name": "Kannada"
305 | },
306 | {
307 | "urlParam": "kr",
308 | "name": "Kanuri"
309 | },
310 | {
311 | "urlParam": "ks",
312 | "name": "Kashmiri"
313 | },
314 | {
315 | "urlParam": "kk",
316 | "name": "Kazakh"
317 | },
318 | {
319 | "urlParam": "km",
320 | "name": "Central Khmer"
321 | },
322 | {
323 | "urlParam": "ki",
324 | "name": "Kikuyu, Gikuyu"
325 | },
326 | {
327 | "urlParam": "rw",
328 | "name": "Kinyarwanda"
329 | },
330 | {
331 | "urlParam": "ky",
332 | "name": "Kirghiz, Kyrgyz"
333 | },
334 | {
335 | "urlParam": "kv",
336 | "name": "Komi"
337 | },
338 | {
339 | "urlParam": "kg",
340 | "name": "Kongo"
341 | },
342 | {
343 | "urlParam": "ko",
344 | "name": "Korean"
345 | },
346 | {
347 | "urlParam": "ku",
348 | "name": "Kurdish"
349 | },
350 | {
351 | "urlParam": "kj",
352 | "name": "Kuanyama, Kwanyama"
353 | },
354 | {
355 | "urlParam": "la",
356 | "name": "Latin"
357 | },
358 | {
359 | "urlParam": "lb",
360 | "name": "Luxembourgish, Letzeburgesch"
361 | },
362 | {
363 | "urlParam": "lg",
364 | "name": "Ganda"
365 | },
366 | {
367 | "urlParam": "li",
368 | "name": "Limburgan, Limburger, Limburgish"
369 | },
370 | {
371 | "urlParam": "ln",
372 | "name": "Lingala"
373 | },
374 | {
375 | "urlParam": "lo",
376 | "name": "Lao"
377 | },
378 | {
379 | "urlParam": "lt",
380 | "name": "Lithuanian"
381 | },
382 | {
383 | "urlParam": "lu",
384 | "name": "Luba-Katanga"
385 | },
386 | {
387 | "urlParam": "lv",
388 | "name": "Latvian"
389 | },
390 | {
391 | "urlParam": "gv",
392 | "name": "Manx"
393 | },
394 | {
395 | "urlParam": "mk",
396 | "name": "Macedonian"
397 | },
398 | {
399 | "urlParam": "mg",
400 | "name": "Malagasy"
401 | },
402 | {
403 | "urlParam": "ms",
404 | "name": "Malay"
405 | },
406 | {
407 | "urlParam": "ml",
408 | "name": "Malayalam"
409 | },
410 | {
411 | "urlParam": "mt",
412 | "name": "Maltese"
413 | },
414 | {
415 | "urlParam": "mi",
416 | "name": "Maori"
417 | },
418 | {
419 | "urlParam": "mr",
420 | "name": "Marathi"
421 | },
422 | {
423 | "urlParam": "mh",
424 | "name": "Marshallese"
425 | },
426 | {
427 | "urlParam": "mn",
428 | "name": "Mongolian"
429 | },
430 | {
431 | "urlParam": "na",
432 | "name": "Nauru"
433 | },
434 | {
435 | "urlParam": "nv",
436 | "name": "Navajo, Navaho"
437 | },
438 | {
439 | "urlParam": "nd",
440 | "name": "North Ndebele"
441 | },
442 | {
443 | "urlParam": "ne",
444 | "name": "Nepali"
445 | },
446 | {
447 | "urlParam": "ng",
448 | "name": "Ndonga"
449 | },
450 | {
451 | "urlParam": "nb",
452 | "name": "Norwegian Bokmål"
453 | },
454 | {
455 | "urlParam": "nn",
456 | "name": "Norwegian Nynorsk"
457 | },
458 | {
459 | "urlParam": "no",
460 | "name": "Norwegian"
461 | },
462 | {
463 | "urlParam": "ii",
464 | "name": "Sichuan Yi, Nuosu"
465 | },
466 | {
467 | "urlParam": "nr",
468 | "name": "South Ndebele"
469 | },
470 | {
471 | "urlParam": "oc",
472 | "name": "Occitan"
473 | },
474 | {
475 | "urlParam": "oj",
476 | "name": "Ojibwa"
477 | },
478 | {
479 | "urlParam": "cu",
480 | "name": "Church Slavic, Old Slavonic, Chu..."
481 | },
482 | {
483 | "urlParam": "om",
484 | "name": "Oromo"
485 | },
486 | {
487 | "urlParam": "or",
488 | "name": "Oriya"
489 | },
490 | {
491 | "urlParam": "os",
492 | "name": "Ossetian, Ossetic"
493 | },
494 | {
495 | "urlParam": "pa",
496 | "name": "Punjabi, Panjabi"
497 | },
498 | {
499 | "urlParam": "pi",
500 | "name": "Pali"
501 | },
502 | {
503 | "urlParam": "fa",
504 | "name": "Persian"
505 | },
506 | {
507 | "urlParam": "pl",
508 | "name": "Polish"
509 | },
510 | {
511 | "urlParam": "ps",
512 | "name": "Pashto, Pushto"
513 | },
514 | {
515 | "urlParam": "pt",
516 | "name": "Portuguese"
517 | },
518 | {
519 | "urlParam": "qu",
520 | "name": "Quechua"
521 | },
522 | {
523 | "urlParam": "rm",
524 | "name": "Romansh"
525 | },
526 | {
527 | "urlParam": "rn",
528 | "name": "Rundi"
529 | },
530 | {
531 | "urlParam": "ro",
532 | "name": "Romanian, Moldavian, Moldovan"
533 | },
534 | {
535 | "urlParam": "ru",
536 | "name": "Russian"
537 | },
538 | {
539 | "urlParam": "sa",
540 | "name": "Sanskrit"
541 | },
542 | {
543 | "urlParam": "sc",
544 | "name": "Sardinian"
545 | },
546 | {
547 | "urlParam": "sd",
548 | "name": "Sindhi"
549 | },
550 | {
551 | "urlParam": "se",
552 | "name": "Northern Sami"
553 | },
554 | {
555 | "urlParam": "sm",
556 | "name": "Samoan"
557 | },
558 | {
559 | "urlParam": "sg",
560 | "name": "Sango"
561 | },
562 | {
563 | "urlParam": "sr",
564 | "name": "Serbian"
565 | },
566 | {
567 | "urlParam": "gd",
568 | "name": "Gaelic, Scottish Gaelic"
569 | },
570 | {
571 | "urlParam": "sn",
572 | "name": "Shona"
573 | },
574 | {
575 | "urlParam": "si",
576 | "name": "Sinhala, Sinhalese"
577 | },
578 | {
579 | "urlParam": "sk",
580 | "name": "Slovak"
581 | },
582 | {
583 | "urlParam": "sl",
584 | "name": "Slovenian"
585 | },
586 | {
587 | "urlParam": "so",
588 | "name": "Somali"
589 | },
590 | {
591 | "urlParam": "st",
592 | "name": "Southern Sotho"
593 | },
594 | {
595 | "urlParam": "es",
596 | "name": "Spanish, Castilian"
597 | },
598 | {
599 | "urlParam": "su",
600 | "name": "Sundanese"
601 | },
602 | {
603 | "urlParam": "sw",
604 | "name": "Swahili"
605 | },
606 | {
607 | "urlParam": "ss",
608 | "name": "Swati"
609 | },
610 | {
611 | "urlParam": "sv",
612 | "name": "Swedish"
613 | },
614 | {
615 | "urlParam": "ta",
616 | "name": "Tamil"
617 | },
618 | {
619 | "urlParam": "te",
620 | "name": "Telugu"
621 | },
622 | {
623 | "urlParam": "tg",
624 | "name": "Tajik"
625 | },
626 | {
627 | "urlParam": "th",
628 | "name": "Thai"
629 | },
630 | {
631 | "urlParam": "ti",
632 | "name": "Tigrinya"
633 | },
634 | {
635 | "urlParam": "bo",
636 | "name": "Tibetan"
637 | },
638 | {
639 | "urlParam": "tk",
640 | "name": "Turkmen"
641 | },
642 | {
643 | "urlParam": "tl",
644 | "name": "Tagalog"
645 | },
646 | {
647 | "urlParam": "tn",
648 | "name": "Tswana"
649 | },
650 | {
651 | "urlParam": "to",
652 | "name": "Tonga (Tonga Islands)"
653 | },
654 | {
655 | "urlParam": "tr",
656 | "name": "Turkish"
657 | },
658 | {
659 | "urlParam": "ts",
660 | "name": "Tsonga"
661 | },
662 | {
663 | "urlParam": "tt",
664 | "name": "Tatar"
665 | },
666 | {
667 | "urlParam": "tw",
668 | "name": "Twi"
669 | },
670 | {
671 | "urlParam": "ty",
672 | "name": "Tahitian"
673 | },
674 | {
675 | "urlParam": "ug",
676 | "name": "Uighur, Uyghur"
677 | },
678 | {
679 | "urlParam": "uk",
680 | "name": "Ukrainian"
681 | },
682 | {
683 | "urlParam": "ur",
684 | "name": "Urdu"
685 | },
686 | {
687 | "urlParam": "uz",
688 | "name": "Uzbek"
689 | },
690 | {
691 | "urlParam": "ve",
692 | "name": "Venda"
693 | },
694 | {
695 | "urlParam": "vi",
696 | "name": "Vietnamese"
697 | },
698 | {
699 | "urlParam": "vo",
700 | "name": "Volapük"
701 | },
702 | {
703 | "urlParam": "wa",
704 | "name": "Walloon"
705 | },
706 | {
707 | "urlParam": "cy",
708 | "name": "Welsh"
709 | },
710 | {
711 | "urlParam": "wo",
712 | "name": "Wolof"
713 | },
714 | {
715 | "urlParam": "fy",
716 | "name": "Western Frisian"
717 | },
718 | {
719 | "urlParam": "xh",
720 | "name": "Xhosa"
721 | },
722 | {
723 | "urlParam": "yi",
724 | "name": "Yiddish"
725 | },
726 | {
727 | "urlParam": "yo",
728 | "name": "Yoruba"
729 | },
730 | {
731 | "urlParam": "za",
732 | "name": "Zhuang, Chuang"
733 | },
734 | {
735 | "urlParam": "zu",
736 | "name": "Zulu"
737 | }
738 | ]
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | def pytest_addoption(parser):
5 | parser.addoption(
6 | "--auth",
7 | action="store",
8 | default="",
9 | help="username:token to pass to test functions",
10 | )
11 |
12 |
13 | @pytest.fixture(scope="class")
14 | def auth(request):
15 | request.cls.auth = request.config.getoption("--auth")
16 | return request.config.getoption("--auth")
17 |
18 |
19 | def pytest_configure(config):
20 | config.addinivalue_line(
21 | "markers", "auth: mark test as requiring authentication token"
22 | )
23 |
24 |
25 | def pytest_collection_modifyitems(config, items):
26 | if config.getoption("--auth"):
27 | # --auth given in cli: do not skip auth-required tests
28 | return
29 | skip_auth = pytest.mark.skip(reason="need --auth option to run")
30 | for item in items:
31 | if "auth" in item.keywords:
32 | item.add_marker(skip_auth)
33 |
--------------------------------------------------------------------------------
/tests/test_cli.py:
--------------------------------------------------------------------------------
1 | """tests.test_cli"""
2 |
3 | from datetime import datetime, timedelta
4 | from random import randint
5 | from sys import maxsize
6 | from time import time
7 | # import re
8 | import os
9 | import shutil
10 |
11 | from click.testing import CliRunner
12 | import pytest
13 |
14 | from starcli.__main__ import cli, CACHE_DIR, CACHED_RESULT_PATH
15 |
16 |
17 | @pytest.mark.usefixtures("auth")
18 | class TestCli:
19 | def test_cli_debug(self):
20 | """Test cli when --debug is passed"""
21 | result = self.cli_result(debug=True)
22 | self.assertions(result, debug=True)
23 |
24 | def test_cli(self):
25 | """Test cli when no commands given & debug+auth off"""
26 | result = self.cli_result(debug=False, auth="")
27 | self.assertions(result, debug=False)
28 |
29 | @pytest.mark.auth # This test needs --auth, otherwise skip
30 | def test_auth(self, auth):
31 | """Test basic authentication for valid auth credentials"""
32 | result = self.cli_result(auth=auth)
33 | self.assertions(
34 | result,
35 | # Testing rich logger output doesn't work
36 | # in_stderr=("DEBUG: auth: on",),
37 | # not_in_stderr=("The server did not accept the credentials.",),
38 | )
39 |
40 | # def test_no_auth(self):
41 | # """Test without --auth"""
42 | # result = self.cli_result(auth="")
43 | # Testing rich logger output doesn't work
44 | # self.assertions(result, in_stderr=("DEBUG: auth: off",))
45 |
46 | # def test_incorrect_auth(self):
47 | # """Test incorrect credentials provided to --auth"""
48 | # result = self.cli_result(auth="github:0000")
49 | # self.assertions(
50 | # result,
51 | # # Testing rich logger output doesn't work
52 | # # in_stderr=("The server did not accept the credentials.",),
53 | # )
54 |
55 | def test_invalid_auth_format(self):
56 | """Test invalid credentials provided to --auth"""
57 | # XXX: --auth format not checked if a cache exists
58 | result = self.cli_result(auth="github:", clear_cache=True, nop=True)
59 | self.assertions(result, in_output=("Invalid authentication format",))
60 |
61 | result = self.cli_result(auth=":0000", clear_cache=True, nop=True)
62 | self.assertions(result, in_output=("Invalid authentication format",))
63 |
64 | result = self.cli_result(auth="abc", clear_cache=True, nop=True)
65 | self.assertions(result, in_output=("Invalid authentication format",))
66 |
67 | def test_cli_lang(self):
68 | """Test cli when --lang or -l is passed"""
69 | param_decls = ["--lang", "-l"]
70 |
71 | for param in param_decls:
72 | result = self.cli_result(param, "python", clear_cache=False)
73 | self.assertions(
74 | result,
75 | # Testing rich logger output doesn't work
76 | # in_stderr=("language:python",),
77 | )
78 |
79 | # Until upstream github trending dependency is fixed
80 | @pytest.mark.xfail()
81 | def test_cli_spoken_language(self):
82 | """Test cli when --spoken-language or -S is passed"""
83 | param_decls = ["--spoken-language", "-S"]
84 | # Currently, this option uses `search_github_trending`, which produces no 'DEBUG:'
85 | for param in param_decls:
86 | result = self.cli_result(param, "en")
87 | self.assertions(result, debug=False)
88 |
89 | def test_cli_created(self):
90 | """Test cli when --created or -c with valid option is passed"""
91 | param_decls = ["--created", "-c"]
92 |
93 | for param in param_decls:
94 | date_format = "%Y-%m-%d"
95 | day_range = 0 - randint(100, 400)
96 | created_date_value = ">=" + (
97 | datetime.utcnow() + timedelta(days=day_range)
98 | ).strftime(date_format)
99 | result = self.cli_result(param, created_date_value)
100 | self.assertions(result, not_in_output=("Invalid date",))
101 |
102 | def test_cli_created_invalid(self):
103 | """Test cli when --created or -c with invalid option is passed"""
104 | param_decls = ["--created", "-c"]
105 |
106 | for param in param_decls:
107 | date_format = "%d-%m-%Y"
108 | day_range = 0 - randint(100, 400)
109 | created_date_value = (
110 | datetime.utcnow() + timedelta(days=day_range)
111 | ).strftime(date_format)
112 | result = self.cli_result(param, created_date_value)
113 | self.assertions(
114 | result,
115 | in_output=("Invalid date",),
116 | )
117 |
118 | def test_cli_topic(self):
119 | """Test cli when --topic or -t is passed"""
120 | param_decls = ["--topic", "-t"]
121 |
122 | for param in param_decls:
123 | result = self.cli_result(
124 | param, "javascript", param, "nodejs", clear_cache=False
125 | ) # javascript + nodejs will likely come up together
126 | self.assertions(result)
127 |
128 | def test_cli_pushed(self):
129 | """Test cli when --pushed or -p is passed"""
130 | param_decls = ["--pushed", "-p"]
131 |
132 | for param in param_decls:
133 | date_format = "%Y-%m-%d"
134 | day_range = 0 - randint(100, 400)
135 | pushed_date_value = (
136 | datetime.utcnow() + timedelta(days=day_range)
137 | ).strftime(date_format)
138 | result = self.cli_result(param, pushed_date_value)
139 | self.assertions(result, not_in_output=("Invalid date",))
140 |
141 | def test_cli_pushed_invalid(self):
142 | """Test cli when invalid option to --pushed or -p is passed"""
143 | param_decls = ["--pushed", "-p"]
144 |
145 | for param in param_decls:
146 | date_format = "%d-%m-%Y"
147 | day_range = 0 - randint(100, 400)
148 | pushed_date_value = (
149 | datetime.utcnow() + timedelta(days=day_range)
150 | ).strftime(date_format)
151 | result = self.cli_result(param, pushed_date_value, clear_cache=False)
152 | self.assertions(
153 | result,
154 | in_output=("Invalid date"),
155 | )
156 |
157 | def test_cli_layout(self):
158 | """Test cli when --layout or -L is passed"""
159 | param_decls = ["--layout", "-L"]
160 | choices = ["list", "table", "grid"]
161 |
162 | for param in param_decls:
163 | for choice in choices:
164 | result = self.cli_result(param, choice, clear_cache=False)
165 | self.assertions(result)
166 |
167 | def test_cli_stars(self):
168 | """Test cli when --stars or -s is passed"""
169 | param_decls = ["--stars", "-s"]
170 |
171 | for param in param_decls:
172 | result = self.cli_result(param, 0)
173 | self.assertions(result)
174 |
175 | result = self.cli_result(param, 1)
176 | self.assertions(result)
177 |
178 | result = self.cli_result(param, maxsize)
179 | self.assertions(result)
180 |
181 | def test_cli_limit_results(self):
182 | """Test cli when --limit-results or -r is passed"""
183 | param_decls = ["--limit-results", "-r"]
184 |
185 | for param in param_decls:
186 | result = self.cli_result(param, 0, clear_cache=False)
187 | self.assertions(result)
188 |
189 | result = self.cli_result(param, 1, clear_cache=False)
190 | self.assertions(result)
191 |
192 | result = self.cli_result(param, maxsize, clear_cache=False)
193 | self.assertions(result)
194 |
195 | result = self.cli_result(param, -1, clear_cache=False)
196 | self.assertions(result)
197 |
198 | def test_cli_order(self):
199 | """Test cli when --order or -o is passed"""
200 | param_decls = ["--order", "-o"]
201 | choices = ["desc", "asc"]
202 |
203 | for param in param_decls:
204 | for choice in choices:
205 | result = self.cli_result(param, choice, clear_cache=False)
206 | self.assertions(result)
207 |
208 | def test_cli_long_stats(self):
209 | """Test cli when --long-stats is passed"""
210 | result = self.cli_result("--long-stats", clear_cache=False)
211 | self.assertions(result)
212 |
213 | @pytest.mark.xfail()
214 | def test_cli_date_range(self):
215 | """Test cli when --date-range or -d is passed"""
216 | param_decls = ["--date-range", "-d"]
217 | choices = ["today", "this-week", "this-month"]
218 | # Currently, this option uses `search_github_trending`, which produces no 'DEBUG:'
219 | for param in param_decls:
220 | for choice in choices:
221 | result = self.cli_result(param, choice)
222 | self.assertions(result, debug=False)
223 |
224 | def test_cli_user(self):
225 | """Test cli when --user or -U is passed"""
226 | param_decls = ["--user", "-u"]
227 |
228 | for param in param_decls:
229 | result = self.cli_result(param, "github", clear_cache=False)
230 | self.assertions(result)
231 |
232 | def test_cached_file_existence(self):
233 | """Test the caching of result"""
234 |
235 | self.cli_result(
236 | "--topic", "python", "--stars", ">100", clear_cache=True, nop=False
237 | )
238 |
239 | assert os.path.exists(CACHE_DIR), f"Cache directory not created: {CACHE_DIR}"
240 | assert os.path.exists(
241 | CACHED_RESULT_PATH
242 | ), f"Failed to create cache file: {CACHED_RESULT_PATH}"
243 |
244 | def test_time_diff_for_cached_result(self):
245 | """Test the time difference between fetching new and cached result"""
246 |
247 | start = time()
248 | self.cli_result("--topic", "python", "--stars", ">1000", clear_cache=True)
249 | end = time()
250 | new_result_runtime = end - start
251 |
252 | start = time()
253 | self.cli_result("--topic", "python", "--stars", ">1000", clear_cache=False)
254 | end = time()
255 | cached_result_runtime = end - start
256 |
257 | assert (
258 | new_result_runtime > cached_result_runtime
259 | ), f"Cached result took longer ({cached_result_runtime}) than newly fetching results ({new_result_runtime})."
260 |
261 | def cli_result(
262 | self,
263 | *args,
264 | debug=True,
265 | auth="",
266 | clear_cache=True,
267 | nop=False,
268 | ):
269 | """
270 | CliRunner() helper function. Returns a `click.testing.Result` object.
271 | Passes `--debug` by default. Passes `--auth` + credentials, if given.
272 |
273 | Also clear the cache if needed.
274 | """
275 | if clear_cache:
276 | try:
277 | shutil.rmtree(CACHE_DIR)
278 | except FileNotFoundError:
279 | pass
280 |
281 | runner = CliRunner()
282 | cli_params = list(args)
283 |
284 | if auth:
285 | cli_params.extend(["--auth", auth])
286 | elif self.auth:
287 | cli_params.extend(["--auth", self.auth])
288 |
289 | if debug:
290 | cli_params.append("--debug")
291 |
292 | if nop:
293 | cli_params.append("--nop")
294 |
295 |
296 | return runner.invoke(cli, cli_params) if cli_params else runner.invoke(cli)
297 |
298 | def assertions(
299 | self,
300 | result,
301 | exit_code=0,
302 | output=True,
303 | debug=True,
304 | in_output=(),
305 | not_in_output=(),
306 | in_stderr=(),
307 | not_in_stderr=(),
308 | ):
309 | """
310 | Helper function for basic assert statements.
311 | """
312 | if exit_code:
313 | assert result.exit_code == exit_code, f"`exit_code` should be '{exit_code}'"
314 | # if debug: # logs aren't captured in result.output so it doesn't work
315 | # assert "DEBUG" in result.output, f"'DEBUG' not in `result.output`"
316 | if output and not debug:
317 | assert result.output, "No cli output generated"
318 | elif not output:
319 | assert not result.output, "Cli output generated, but expected nothing"
320 |
321 | if in_output:
322 | for s in in_output:
323 | assert s in result.output, f"'{s}' not found in `result.output.`"
324 | if not_in_output:
325 | for s in not_in_output:
326 | assert (
327 | not s in result.output
328 | ), f"{s} found in `result.output`, but shouldn't be."
329 | if in_stderr:
330 | for s in in_stderr:
331 | assert s in result.stderr, f"{s} not in `result.stderr`"
332 | if not_in_stderr:
333 | for s in not_in_stderr:
334 | assert not s in result.stderr, f"{s} shouldn't be in stderr"
335 |
--------------------------------------------------------------------------------
/tests/test_search.py:
--------------------------------------------------------------------------------
1 | """tests.test_search"""
2 |
3 | from datetime import datetime, timedelta
4 | from random import randint
5 |
6 | import pytest
7 |
8 | from starcli.search import search, search_github_trending
9 |
10 |
11 | def test_search_language():
12 | """Test searching by language"""
13 | for language in ["python", "Python", "JavaScript", "c"]:
14 | repos = search([language])
15 | for repo in repos:
16 | assert repo["stargazers_count"] >= 0
17 | assert repo["forks"] >= 0
18 | assert repo["language"].lower() == language.lower()
19 | assert (repo["description"] is None) or repo["description"]
20 | assert repo["full_name"].count("/") == 1
21 | assert repo["full_name"] == f"{repo['owner']['login']}/{repo['name']}"
22 | assert repo["html_url"] == "https://github.com/" + repo["full_name"]
23 |
24 |
25 | def test_search_topics():
26 | """Test searching by topics"""
27 | for topics in [
28 | "deezer",
29 | "django",
30 | "cookiecutter",
31 | ["web", "flask"],
32 | "python3",
33 | "algorithm",
34 | "shell",
35 | ]:
36 | repos = search(languages=["python"], topics=topics)
37 | for repo in repos:
38 | assert repo["stargazers_count"] >= 0
39 | assert repo["forks"] >= 0
40 | assert repo["language"].lower() == "python"
41 | assert (repo["description"] is None) or repo["description"]
42 | assert repo["full_name"].count("/") >= 1
43 | assert repo["full_name"] == f"{repo['owner']['login']}/{repo['name']}"
44 | assert repo["html_url"] == "https://github.com/" + repo["full_name"]
45 | assert topics in repo["topics"]
46 |
47 |
48 | def test_search_created_date():
49 | """Test searching with creation date"""
50 | date_format = "%Y-%m-%d"
51 | day_range = 0 - randint(100, 400)
52 | created_date_value = (datetime.utcnow() + timedelta(days=day_range)).strftime(
53 | date_format
54 | )
55 | repos = search(languages=["python"], created=created_date_value)
56 | for repo in repos:
57 | assert repo["stargazers_count"] >= 0
58 | assert repo["forks"] >= 0
59 | assert (repo["description"] is None) or repo["description"]
60 | assert repo["full_name"].count("/") >= 1
61 | assert repo["full_name"] == f"{repo['owner']['login']}/{repo['name']}"
62 | assert repo["html_url"] == "https://github.com/" + repo["full_name"]
63 |
64 | assert datetime.strptime(
65 | repo["created_at"].split("T")[0], date_format
66 | ) >= datetime.strptime(created_date_value, date_format) and datetime.strptime(
67 | repo["created_at"].split("T")[0], date_format
68 | ) <= datetime.strptime(
69 | created_date_value, date_format
70 | ) + timedelta(
71 | days=1
72 | )
73 |
74 |
75 | def test_search_pushed_date():
76 | """Test searching with updated date"""
77 | date_format = "%Y-%m-%d"
78 | day_range = 0 - randint(100, 400)
79 | pushed_date_value = (datetime.utcnow() + timedelta(days=day_range)).strftime(
80 | date_format
81 | )
82 | repos = search(languages=["python"], pushed=pushed_date_value)
83 | for repo in repos:
84 | assert repo["stargazers_count"] >= 0
85 | assert repo["forks"] >= 0
86 | assert (repo["description"] is None) or repo["description"]
87 | assert repo["full_name"].count("/") >= 1
88 | assert repo["full_name"] == f"{repo['owner']['login']}/{repo['name']}"
89 | assert repo["html_url"] == "https://github.com/" + repo["full_name"]
90 |
91 | # Need to account for min and max updated dates
92 | assert datetime.strptime(
93 | repo["pushed_at"].split("T")[0], date_format
94 | ) >= datetime.strptime(pushed_date_value, date_format) and datetime.strptime(
95 | repo["pushed_at"].split("T")[0], date_format
96 | ) <= datetime.strptime(
97 | pushed_date_value, date_format
98 | ) + timedelta(
99 | days=1
100 | )
101 |
102 |
103 | @pytest.mark.xfail(raises=AssertionError)
104 | def test_search_stars():
105 | """Test searching with number of stars"""
106 | repos = search(languages=["python"], stars="<10")
107 | for repo in repos:
108 | # FIXME: Possibly problem with GitHub API?
109 | assert repo["stargazers_count"] < 10 # Sometimes stars+1
110 | assert repo["forks"] >= 0
111 | assert (repo["description"] is None) or repo["description"]
112 | assert repo["full_name"].count("/") >= 1
113 | assert repo["full_name"] == f"{repo['owner']['login']}/{repo['name']}"
114 | assert repo["html_url"] == "https://github.com/" + repo["full_name"]
115 |
116 |
117 | def test_search_user():
118 | """Test searching by user"""
119 | repos = search(languages=["ruby"], user="octocat")
120 | for repo in repos:
121 | assert repo["stargazers_count"] >= 0
122 | assert repo["forks"] >= 0
123 | assert (repo["description"] is None) or repo["description"]
124 | assert repo["full_name"].split("/")[0] == "octocat"
125 | assert repo["full_name"] == f"{repo['owner']['login']}/{repo['name']}"
126 | assert repo["html_url"] == "https://github.com/" + repo["full_name"]
127 |
128 |
129 | def test_no_results():
130 | """Test if no search results found"""
131 | repos = search(["python"], "2020-01-01", "2019-01-01")
132 | assert repos == []
133 |
134 |
135 | # commented until upstream github trending dependency is fixed
136 | @pytest.mark.xfail()
137 | def test_spoken_language():
138 | """Test search by spoken languages"""
139 | repos = search_github_trending(["javascript"], "zh") # zh = chinese
140 | for repo in repos:
141 | assert repo["stargazers_count"] >= 0
142 | assert repo["forks"] >= 0
143 | assert repo["language"].lower() == "javascript"
144 | assert (repo["description"] == None) or repo["description"]
145 | assert repo["full_name"].count("/") >= 1
146 | assert repo["html_url"] == "https://github.com/" + repo["full_name"]
147 |
148 | @pytest.mark.xfail()
149 | def test_date_range():
150 | """Test search by date range"""
151 | for date_range in ["daily", "monthly", "weekly"]:
152 | repos = search_github_trending(["python"], "en", date_range)
153 | for repo in repos:
154 | assert repo["stargazers_count"] >= 0
155 | assert repo["forks"] >= 0
156 | assert repo["language"].lower() == "python"
157 | assert (repo["description"] == None) or repo["description"]
158 | assert repo["full_name"].count("/") >= 1
159 | assert repo["html_url"] == "https://github.com/" + repo["full_name"]
160 | # TODO: verify date range
161 |
162 |
163 | def test_search_multiple_language():
164 | """Test searching by multiple language"""
165 | languages = ["python", "c"]
166 | repos = search(languages)
167 | for repo in repos:
168 | assert repo["stargazers_count"] >= 0
169 | assert repo["forks"] >= 0
170 | assert repo["language"].lower() in languages
171 | assert (repo["description"] is None) or repo["description"]
172 | assert repo["full_name"].count("/") == 1
173 | assert repo["full_name"] == f"{repo['owner']['login']}/{repo['name']}"
174 | assert repo["html_url"] == "https://github.com/" + repo["full_name"]
175 |
--------------------------------------------------------------------------------
/tests/test_shorten_count.py:
--------------------------------------------------------------------------------
1 | """tests.test_shorten_count"""
2 | from starcli.layouts import shorten_count
3 |
4 |
5 | def test_shorten_count():
6 | """Test the shorten_count functionality"""
7 | assert shorten_count(1487) == "1.5k"
8 | assert shorten_count(6001) == "6k"
9 | assert shorten_count(15587) == "15.6k"
10 | assert shorten_count(12) == "12"
11 |
--------------------------------------------------------------------------------