sharex-cli 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ # Generic
2
+ .idea/
3
+ *.iml
4
+ .vscode/
5
+ .venv/
6
+ venv/
7
+ __pycache__/
8
+ *.egg-info/
9
+ build/
10
+ dist/
11
+ .*cache/
12
+ *.log
13
+ *.pyc
14
+ .coverage
15
+ coverage.xml
16
+ # App
17
+ /site/
18
+ /tests/files
19
+ /tests/fake.json
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 CSSNR
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.
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.4
2
+ Name: sharex-cli
3
+ Version: 0.0.1
4
+ Summary: CLI to Upload Files to a ShareX Server using a ShareX Custom Uploader (.sxcu) Configuration File.
5
+ Project-URL: Changelog, https://github.com/cssnr/sharex-cli/releases
6
+ Project-URL: Discord, https://discord.gg/wXy6m2X8wY
7
+ Project-URL: Documentation, https://cssnr.github.io/sharex-cli/
8
+ Project-URL: Funding, https://ko-fi.com/cssnr
9
+ Project-URL: GitHub, https://github.com/cssnr/sharex-cli
10
+ Project-URL: Homepage, https://cssnr.com/
11
+ Project-URL: Issues, https://github.com/cssnr/sharex-cli/issues
12
+ Author: Shane
13
+ License: MIT License
14
+ License-File: LICENSE
15
+ Classifier: Natural Language :: English
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: jsonpath-ng<2,>=1
23
+ Requires-Dist: pyperclip<2,>=1
24
+ Requires-Dist: requests<3,>=2
25
+ Requires-Dist: typer>=0.20.0
26
+ Description-Content-Type: text/markdown
27
+
28
+ [![PyPI Version](https://img.shields.io/pypi/v/sharex-cli?logo=pypi&logoColor=white&label=pypi)](https://pypi.org/project/sharex-cli/)
29
+ [![GitHub Release Version](https://img.shields.io/github/v/release/cssnr/sharex-cli?logo=github)](https://github.com/cssnr/sharex-cli/releases)
30
+ [![TOML Python Version](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Fcssnr%2Fsharex-cli%2Frefs%2Fheads%2Fmaster%2Fpyproject.toml&query=%24.project.requires-python&logo=python&logoColor=white&label=python)](https://github.com/cssnr/sharex-cli?tab=readme-ov-file#readme)
31
+ [![PyPI Downloads](https://img.shields.io/pypi/dm/sharex-cli?logo=pypi&logoColor=white)](https://pypistats.org/packages/sharex-cli)
32
+ [![Pepy Total Downloads](https://img.shields.io/pepy/dt/sharex-cli?logo=pypi&logoColor=white&label=total)](https://clickpy.clickhouse.com/dashboard/sharex-cli)
33
+ [![Codecov](https://codecov.io/gh/cssnr/sharex-cli/graph/badge.svg?token=A8NDHZ393X)](https://codecov.io/gh/cssnr/sharex-cli)
34
+ [![Workflow Lint](https://img.shields.io/github/actions/workflow/status/cssnr/sharex-cli/lint.yaml?logo=cachet&label=lint)](https://github.com/cssnr/sharex-cli/actions/workflows/lint.yaml)
35
+ [![Workflow Test](https://img.shields.io/github/actions/workflow/status/cssnr/sharex-cli/test.yaml?logo=cachet&label=test)](https://github.com/cssnr/sharex-cli/actions/workflows/test.yaml)
36
+ [![Deployment PyPi](https://img.shields.io/github/deployments/cssnr/sharex-cli/pypi?logo=pypi&logoColor=white&label=pypi)](https://pypi.org/project/sharex-cli/)
37
+ [![Deployment Docs](https://img.shields.io/github/deployments/cssnr/sharex-cli/docs?logo=materialformkdocs&logoColor=white&label=docs)](https://cssnr.github.io/sharex-cli/)
38
+ [![GitHub Last Commit](https://img.shields.io/github/last-commit/cssnr/sharex-cli?logo=github&label=updated)](https://github.com/cssnr/sharex-cli/pulse)
39
+ [![GitHub Repo Size](https://img.shields.io/github/repo-size/cssnr/sharex-cli?logo=bookstack&logoColor=white&label=repo%20size)](https://github.com/cssnr/sharex-cli)
40
+ [![GitHub Top Language](https://img.shields.io/github/languages/top/cssnr/sharex-cli?logo=htmx&logoColor=white)](https://github.com/cssnr/sharex-cli?tab=readme-ov-file#readme)
41
+ [![GitHub Contributors](https://img.shields.io/github/contributors-anon/cssnr/sharex-cli?logo=github)](https://github.com/cssnr/sharex-cli/graphs/contributors)
42
+ [![GitHub Issues](https://img.shields.io/github/issues/cssnr/sharex-cli?logo=github)](https://github.com/cssnr/sharex-cli/issues)
43
+ [![GitHub Discussions](https://img.shields.io/github/discussions/cssnr/sharex-cli?logo=github)](https://github.com/cssnr/sharex-cli/discussions)
44
+ [![GitHub Forks](https://img.shields.io/github/forks/cssnr/sharex-cli?style=flat&logo=github)](https://github.com/cssnr/sharex-cli/forks)
45
+ [![GitHub Repo Stars](https://img.shields.io/github/stars/cssnr/sharex-cli?style=flat&logo=github)](https://github.com/cssnr/sharex-cli/stargazers)
46
+ [![GitHub Org Stars](https://img.shields.io/github/stars/cssnr?style=flat&logo=github&label=org%20stars)](https://cssnr.github.io/)
47
+ [![Discord](https://img.shields.io/discord/899171661457293343?logo=discord&logoColor=white&label=discord&color=7289da)](https://discord.gg/wXy6m2X8wY)
48
+ [![Ko-fi](https://img.shields.io/badge/Ko--fi-72a5f2?logo=kofi&label=support)](https://ko-fi.com/cssnr)
49
+
50
+ # ShareX CLI
51
+
52
+ <a title="ShareX CLI" href="https://cssnr.github.io/sharex-cli/" target="_blank">
53
+ <img alt="ShareX CLI" align="right" width="128" height="auto" src="https://cssnr.github.io/sharex-cli/assets/images/logo.svg"></a>
54
+
55
+ - [Features](#features)
56
+ - [Install](#install)
57
+ - [Setup](#setup)
58
+ - [Usage](#usage)
59
+ - [Support](#support)
60
+ - [Contributing](#contributing)
61
+
62
+ Command Line Interface to Upload Files to a ShareX Server using a ShareX Custom Uploader `.sxcu` configuration file.
63
+
64
+ Upload any file, multiple files, directories, use globs, create archives and much more...
65
+
66
+ To get started [Install](#install) the app and view the [Usage](#usage).
67
+
68
+ If you run into any issues or have any questions, [support](#support) is available.
69
+
70
+ [![View Documentation](https://img.shields.io/badge/view_documentation-blue?style=for-the-badge&logo=googledocs&logoColor=white)](https://cssnr.github.io/sharex-cli/)
71
+
72
+ ## Features<a id="features"></a>
73
+
74
+ - Use a ShareX Custom Uploader `*.sxcu` config.
75
+ - Upload a file or multiple files.
76
+ - Specify a custom file name.
77
+ - Upload files in a directory with optional glob.
78
+ - Upload a directory as an archive.
79
+ - Automatically open the URL in browser.
80
+ - Automatically copy the URL to the clipboard.
81
+ - Display confirmation before uploading multiple files.
82
+ - Override all options with flags or env vars.
83
+
84
+ ## Install<a id="install"></a>
85
+
86
+ From PyPI: <https://pypi.org/p/sharex-cli>
87
+
88
+ ```shell
89
+ pip install sharex-cli
90
+ ```
91
+
92
+ From GitHub.
93
+
94
+ ```shell
95
+ pip install git+https://github.com/cssnr/sharex-cli.git
96
+ ```
97
+
98
+ From source.
99
+
100
+ ```shell
101
+ git clone https://github.com/cssnr/sharex-cli.git
102
+ pip install sharex-cli
103
+ ```
104
+
105
+ [![View Install Guide](https://img.shields.io/badge/view_install_guide-blue?style=for-the-badge&logo=googledocs&logoColor=white)](https://cssnr.github.io/sharex-cli/cli/#install)
106
+
107
+ ## Setup<a id="setup"></a>
108
+
109
+ To configure you need your server's ShareX Custom Uploader `*.sxcu` configuration JSON.
110
+
111
+ The app will automatically enter configuration on the first run.
112
+
113
+ ```shell
114
+ sharex
115
+ ```
116
+
117
+ This allows you to enter a file path, or open a text editor to enter the contents.
118
+
119
+ You can reconfigure with `--config` and pass a path to the config file.
120
+
121
+ ```shell
122
+ sharex --config path/to/config.sxcu
123
+ ```
124
+
125
+ [![View Setup Guide](https://img.shields.io/badge/view_setup_guide-blue?style=for-the-badge&logo=googledocs&logoColor=white)](https://cssnr.github.io/sharex-cli/cli/#setup)
126
+
127
+ ## Usage<a id="usage"></a>
128
+
129
+ Once the configuration is saved you can upload a file, or multiple.
130
+
131
+ ```shell
132
+ sharex file1.txt /path/to/file2.txt
133
+ ```
134
+
135
+ You can upload the contents of a directory, default glob is `*`.
136
+
137
+ ```shell
138
+ sharex dir1
139
+ ```
140
+
141
+ Or create an archive of the directory (no glob support).
142
+
143
+ ```shell
144
+ sharex /path/to/dir2 -a
145
+ ```
146
+
147
+ [![View Uploading Guide](https://img.shields.io/badge/view_usage_examples-blue?style=for-the-badge&logo=googledocs&logoColor=white)](https://cssnr.github.io/sharex-cli/cli/#uploading)
148
+
149
+ ## Support<a id="support"></a>
150
+
151
+ For general help or to request a feature, see:
152
+
153
+ - Q&A Discussion: <https://github.com/cssnr/sharex-cli/discussions/categories/q-a>
154
+ - Request a Feature: <https://github.com/cssnr/sharex-cli/issues/new?template=1-feature.yaml>
155
+ - Chat with us on Discord: <https://discord.gg/wXy6m2X8wY>
156
+
157
+ If you are experiencing an issue/bug or getting unexpected results, you can:
158
+
159
+ - Report an Issue: <https://github.com/cssnr/sharex-cli/issues>
160
+ - Chat with us on Discord: <https://discord.gg/wXy6m2X8wY>
161
+
162
+ [![Features](https://img.shields.io/badge/features-brightgreen?style=for-the-badge&logo=googleanalytics&logoColor=white)](https://github.com/cssnr/sharex-cli/issues/new?template=1-feature.yaml)
163
+ [![Issues](https://img.shields.io/badge/issues-red?style=for-the-badge&logo=southwestairlines&logoColor=white)](https://github.com/cssnr/sharex-cli/issues)
164
+ [![Discussions](https://img.shields.io/badge/discussions-blue?style=for-the-badge&logo=rocketdotchat&logoColor=white)](https://github.com/cssnr/sharex-cli/discussions)
165
+ [![Discord](https://img.shields.io/badge/discord-%235865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/wXy6m2X8wY)
166
+
167
+ ## Contributing<a id="contributing"></a>
168
+
169
+ If you would like to submit a PR, please review the [CONTRIBUTING.md](#contributing-ov-file).
170
+
171
+ Please consider making a donation to support the development of this project
172
+ and [additional](https://cssnr.com/) open source projects.
173
+
174
+ [![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/cssnr)
175
+
176
+ For a full list of current projects visit: [https://cssnr.github.io/](https://cssnr.github.io/)
@@ -0,0 +1,149 @@
1
+ [![PyPI Version](https://img.shields.io/pypi/v/sharex-cli?logo=pypi&logoColor=white&label=pypi)](https://pypi.org/project/sharex-cli/)
2
+ [![GitHub Release Version](https://img.shields.io/github/v/release/cssnr/sharex-cli?logo=github)](https://github.com/cssnr/sharex-cli/releases)
3
+ [![TOML Python Version](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Fcssnr%2Fsharex-cli%2Frefs%2Fheads%2Fmaster%2Fpyproject.toml&query=%24.project.requires-python&logo=python&logoColor=white&label=python)](https://github.com/cssnr/sharex-cli?tab=readme-ov-file#readme)
4
+ [![PyPI Downloads](https://img.shields.io/pypi/dm/sharex-cli?logo=pypi&logoColor=white)](https://pypistats.org/packages/sharex-cli)
5
+ [![Pepy Total Downloads](https://img.shields.io/pepy/dt/sharex-cli?logo=pypi&logoColor=white&label=total)](https://clickpy.clickhouse.com/dashboard/sharex-cli)
6
+ [![Codecov](https://codecov.io/gh/cssnr/sharex-cli/graph/badge.svg?token=A8NDHZ393X)](https://codecov.io/gh/cssnr/sharex-cli)
7
+ [![Workflow Lint](https://img.shields.io/github/actions/workflow/status/cssnr/sharex-cli/lint.yaml?logo=cachet&label=lint)](https://github.com/cssnr/sharex-cli/actions/workflows/lint.yaml)
8
+ [![Workflow Test](https://img.shields.io/github/actions/workflow/status/cssnr/sharex-cli/test.yaml?logo=cachet&label=test)](https://github.com/cssnr/sharex-cli/actions/workflows/test.yaml)
9
+ [![Deployment PyPi](https://img.shields.io/github/deployments/cssnr/sharex-cli/pypi?logo=pypi&logoColor=white&label=pypi)](https://pypi.org/project/sharex-cli/)
10
+ [![Deployment Docs](https://img.shields.io/github/deployments/cssnr/sharex-cli/docs?logo=materialformkdocs&logoColor=white&label=docs)](https://cssnr.github.io/sharex-cli/)
11
+ [![GitHub Last Commit](https://img.shields.io/github/last-commit/cssnr/sharex-cli?logo=github&label=updated)](https://github.com/cssnr/sharex-cli/pulse)
12
+ [![GitHub Repo Size](https://img.shields.io/github/repo-size/cssnr/sharex-cli?logo=bookstack&logoColor=white&label=repo%20size)](https://github.com/cssnr/sharex-cli)
13
+ [![GitHub Top Language](https://img.shields.io/github/languages/top/cssnr/sharex-cli?logo=htmx&logoColor=white)](https://github.com/cssnr/sharex-cli?tab=readme-ov-file#readme)
14
+ [![GitHub Contributors](https://img.shields.io/github/contributors-anon/cssnr/sharex-cli?logo=github)](https://github.com/cssnr/sharex-cli/graphs/contributors)
15
+ [![GitHub Issues](https://img.shields.io/github/issues/cssnr/sharex-cli?logo=github)](https://github.com/cssnr/sharex-cli/issues)
16
+ [![GitHub Discussions](https://img.shields.io/github/discussions/cssnr/sharex-cli?logo=github)](https://github.com/cssnr/sharex-cli/discussions)
17
+ [![GitHub Forks](https://img.shields.io/github/forks/cssnr/sharex-cli?style=flat&logo=github)](https://github.com/cssnr/sharex-cli/forks)
18
+ [![GitHub Repo Stars](https://img.shields.io/github/stars/cssnr/sharex-cli?style=flat&logo=github)](https://github.com/cssnr/sharex-cli/stargazers)
19
+ [![GitHub Org Stars](https://img.shields.io/github/stars/cssnr?style=flat&logo=github&label=org%20stars)](https://cssnr.github.io/)
20
+ [![Discord](https://img.shields.io/discord/899171661457293343?logo=discord&logoColor=white&label=discord&color=7289da)](https://discord.gg/wXy6m2X8wY)
21
+ [![Ko-fi](https://img.shields.io/badge/Ko--fi-72a5f2?logo=kofi&label=support)](https://ko-fi.com/cssnr)
22
+
23
+ # ShareX CLI
24
+
25
+ <a title="ShareX CLI" href="https://cssnr.github.io/sharex-cli/" target="_blank">
26
+ <img alt="ShareX CLI" align="right" width="128" height="auto" src="https://cssnr.github.io/sharex-cli/assets/images/logo.svg"></a>
27
+
28
+ - [Features](#features)
29
+ - [Install](#install)
30
+ - [Setup](#setup)
31
+ - [Usage](#usage)
32
+ - [Support](#support)
33
+ - [Contributing](#contributing)
34
+
35
+ Command Line Interface to Upload Files to a ShareX Server using a ShareX Custom Uploader `.sxcu` configuration file.
36
+
37
+ Upload any file, multiple files, directories, use globs, create archives and much more...
38
+
39
+ To get started [Install](#install) the app and view the [Usage](#usage).
40
+
41
+ If you run into any issues or have any questions, [support](#support) is available.
42
+
43
+ [![View Documentation](https://img.shields.io/badge/view_documentation-blue?style=for-the-badge&logo=googledocs&logoColor=white)](https://cssnr.github.io/sharex-cli/)
44
+
45
+ ## Features<a id="features"></a>
46
+
47
+ - Use a ShareX Custom Uploader `*.sxcu` config.
48
+ - Upload a file or multiple files.
49
+ - Specify a custom file name.
50
+ - Upload files in a directory with optional glob.
51
+ - Upload a directory as an archive.
52
+ - Automatically open the URL in browser.
53
+ - Automatically copy the URL to the clipboard.
54
+ - Display confirmation before uploading multiple files.
55
+ - Override all options with flags or env vars.
56
+
57
+ ## Install<a id="install"></a>
58
+
59
+ From PyPI: <https://pypi.org/p/sharex-cli>
60
+
61
+ ```shell
62
+ pip install sharex-cli
63
+ ```
64
+
65
+ From GitHub.
66
+
67
+ ```shell
68
+ pip install git+https://github.com/cssnr/sharex-cli.git
69
+ ```
70
+
71
+ From source.
72
+
73
+ ```shell
74
+ git clone https://github.com/cssnr/sharex-cli.git
75
+ pip install sharex-cli
76
+ ```
77
+
78
+ [![View Install Guide](https://img.shields.io/badge/view_install_guide-blue?style=for-the-badge&logo=googledocs&logoColor=white)](https://cssnr.github.io/sharex-cli/cli/#install)
79
+
80
+ ## Setup<a id="setup"></a>
81
+
82
+ To configure you need your server's ShareX Custom Uploader `*.sxcu` configuration JSON.
83
+
84
+ The app will automatically enter configuration on the first run.
85
+
86
+ ```shell
87
+ sharex
88
+ ```
89
+
90
+ This allows you to enter a file path, or open a text editor to enter the contents.
91
+
92
+ You can reconfigure with `--config` and pass a path to the config file.
93
+
94
+ ```shell
95
+ sharex --config path/to/config.sxcu
96
+ ```
97
+
98
+ [![View Setup Guide](https://img.shields.io/badge/view_setup_guide-blue?style=for-the-badge&logo=googledocs&logoColor=white)](https://cssnr.github.io/sharex-cli/cli/#setup)
99
+
100
+ ## Usage<a id="usage"></a>
101
+
102
+ Once the configuration is saved you can upload a file, or multiple.
103
+
104
+ ```shell
105
+ sharex file1.txt /path/to/file2.txt
106
+ ```
107
+
108
+ You can upload the contents of a directory, default glob is `*`.
109
+
110
+ ```shell
111
+ sharex dir1
112
+ ```
113
+
114
+ Or create an archive of the directory (no glob support).
115
+
116
+ ```shell
117
+ sharex /path/to/dir2 -a
118
+ ```
119
+
120
+ [![View Uploading Guide](https://img.shields.io/badge/view_usage_examples-blue?style=for-the-badge&logo=googledocs&logoColor=white)](https://cssnr.github.io/sharex-cli/cli/#uploading)
121
+
122
+ ## Support<a id="support"></a>
123
+
124
+ For general help or to request a feature, see:
125
+
126
+ - Q&A Discussion: <https://github.com/cssnr/sharex-cli/discussions/categories/q-a>
127
+ - Request a Feature: <https://github.com/cssnr/sharex-cli/issues/new?template=1-feature.yaml>
128
+ - Chat with us on Discord: <https://discord.gg/wXy6m2X8wY>
129
+
130
+ If you are experiencing an issue/bug or getting unexpected results, you can:
131
+
132
+ - Report an Issue: <https://github.com/cssnr/sharex-cli/issues>
133
+ - Chat with us on Discord: <https://discord.gg/wXy6m2X8wY>
134
+
135
+ [![Features](https://img.shields.io/badge/features-brightgreen?style=for-the-badge&logo=googleanalytics&logoColor=white)](https://github.com/cssnr/sharex-cli/issues/new?template=1-feature.yaml)
136
+ [![Issues](https://img.shields.io/badge/issues-red?style=for-the-badge&logo=southwestairlines&logoColor=white)](https://github.com/cssnr/sharex-cli/issues)
137
+ [![Discussions](https://img.shields.io/badge/discussions-blue?style=for-the-badge&logo=rocketdotchat&logoColor=white)](https://github.com/cssnr/sharex-cli/discussions)
138
+ [![Discord](https://img.shields.io/badge/discord-%235865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/wXy6m2X8wY)
139
+
140
+ ## Contributing<a id="contributing"></a>
141
+
142
+ If you would like to submit a PR, please review the [CONTRIBUTING.md](#contributing-ov-file).
143
+
144
+ Please consider making a donation to support the development of this project
145
+ and [additional](https://cssnr.com/) open source projects.
146
+
147
+ [![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/cssnr)
148
+
149
+ For a full list of current projects visit: [https://cssnr.github.io/](https://cssnr.github.io/)
@@ -0,0 +1,118 @@
1
+ [project]
2
+ name = "sharex-cli"
3
+ description = "CLI to Upload Files to a ShareX Server using a ShareX Custom Uploader (.sxcu) Configuration File."
4
+ readme = "README.md"
5
+ requires-python = ">=3.10"
6
+ license = { text = "MIT License" }
7
+ authors = [{ name = "Shane" }]
8
+ classifiers = [
9
+ "Natural Language :: English",
10
+ "Operating System :: OS Independent",
11
+ "Programming Language :: Python",
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3 :: Only",
14
+ "Typing :: Typed",
15
+ ]
16
+ dependencies = [
17
+ "jsonpath-ng>=1,<2",
18
+ "pyperclip>=1,<2",
19
+ "requests>=2,<3",
20
+ "typer>=0.20.0",
21
+ ]
22
+ dynamic = ["version"]
23
+
24
+ [project.urls]
25
+ Changelog = "https://github.com/cssnr/sharex-cli/releases"
26
+ Discord = "https://discord.gg/wXy6m2X8wY"
27
+ Documentation = "https://cssnr.github.io/sharex-cli/"
28
+ Funding = "https://ko-fi.com/cssnr"
29
+ GitHub = "https://github.com/cssnr/sharex-cli"
30
+ Homepage = "https://cssnr.com/"
31
+ Issues = "https://github.com/cssnr/sharex-cli/issues"
32
+
33
+ [project.scripts]
34
+ sharex = "sharex.cli:app"
35
+
36
+ [dependency-groups]
37
+ dev = [
38
+ "bandit>=1.9.2",
39
+ "black>=25.12.0",
40
+ "coverage>=7.13.0",
41
+ "hatch>=1.16.2",
42
+ "isort>=7.0.0",
43
+ "mypy>=1.19.1",
44
+ "pytest>=9.0.2",
45
+ "ruff>=0.14.9",
46
+ "tombi>=0.7.7",
47
+ "ty>=0.0.2",
48
+ "typer>=0.20.0",
49
+ "types-pyperclip>=1.9.0.20250801",
50
+ "types-requests>=2.32.4.20250913",
51
+ "validate-pyproject[all]>=0.24.1",
52
+ ]
53
+
54
+ # [build-system]
55
+ # requires = ["uv_build>=0.9.9,<0.10.0"]
56
+ # build-backend = "uv_build"
57
+
58
+ # https://hatch.pypa.io/latest/config/build/#build-system
59
+ [build-system]
60
+ requires = ["hatchling"]
61
+ build-backend = "hatchling.build"
62
+
63
+ # Bandit
64
+ # https://bandit.readthedocs.io/en/latest/config.html
65
+ [tool.bandit]
66
+ exclude_dirs = [".venv"]
67
+ skips = ["B105", "B107"]
68
+
69
+ # Black
70
+ # https://black.readthedocs.io/en/stable/usage_and_configuration/
71
+ [tool.black]
72
+ line-length = 119
73
+ extend-exclude = '(\.github)'
74
+
75
+ # Coverage
76
+ # https://coverage.readthedocs.io/en/latest/
77
+ [tool.coverage.run]
78
+ omit = ["_version.py", "utils.py", "cli.py"]
79
+ source = ["src"]
80
+
81
+ # Hatchling
82
+ # https://hatch.pypa.io/latest/config/build/#build-system
83
+ [tool.hatch.version]
84
+ path = "src/sharex/_version.py"
85
+
86
+ [tool.hatch.build.targets.sdist]
87
+ include = ["src", "CONTRIBUTING.md"]
88
+
89
+ [tool.hatch.build.targets.wheel]
90
+ packages = ["src/sharex"]
91
+
92
+ # Isort
93
+ # https://pycqa.github.io/isort/docs/configuration/options.html
94
+ [tool.isort]
95
+ profile = "black"
96
+ lines_after_imports = 2
97
+ src_paths = ["src", "test"]
98
+ skip = [".venv", ".github"]
99
+
100
+ # Mypy
101
+ # https://mypy.readthedocs.io/en/stable/config_file.html
102
+ [tool.mypy]
103
+ # ignore_missing_imports = true
104
+ exclude = [".venv"]
105
+
106
+ [[tool.mypy.overrides]]
107
+ module = ["jsonpath_ng.*"]
108
+ follow_untyped_imports = true
109
+
110
+ # Ruff
111
+ # https://docs.astral.sh/ruff/configuration/
112
+ [tool.ruff]
113
+ line-length = 119
114
+ target-version = "py313"
115
+ extend-exclude = [".github"]
116
+
117
+ [tool.ruff.lint]
118
+ select = ["E4", "E7", "E9", "F", "B", "Q"]
@@ -0,0 +1,12 @@
1
+ r"""
2
+ [red bold] ____ [yellow] _ [cyan]__ __ [green] ____ _ ___
3
+ [red bold] / ___|[yellow]| |__ __ _ _ __ ___[cyan]\ \/ / [green] / ___| | |_ _|
4
+ [red bold] \___ \\[yellow]| '_ \ / _` | '__/ _ \\[cyan]\ / [green]| | | | | |
5
+ [red bold] ___) [yellow]| | | | (_| | | | __/[cyan]/ \ [green]| |___| |___ | |
6
+ [red bold] |____/[yellow]|_| |_|\__,_|_| \___[cyan]/_/\_\ [green] \____|_____|___|
7
+ """
8
+
9
+ from .api import get_config, upload_file
10
+
11
+
12
+ __all__ = ["get_config", "upload_file"]
@@ -0,0 +1,2 @@
1
+ # The string "0.0.1" is used in .github/workflows/build.yaml
2
+ __version__ = "0.0.1"
@@ -0,0 +1,149 @@
1
+ import json
2
+ import mimetypes
3
+ import os
4
+ import warnings
5
+ from json import JSONDecodeError
6
+ from pathlib import Path
7
+ from typing import IO, Optional
8
+
9
+ import requests
10
+ from jsonpath_ng.ext import parse
11
+
12
+ from ._version import __version__
13
+
14
+
15
+ session = requests.Session()
16
+
17
+ session.headers.update({"user-agent": f"requests/sharex-{__version__}"})
18
+
19
+
20
+ def upload_file(config, file_name: str, file_data: IO) -> str:
21
+ """Upload a File from an SXCU Config"""
22
+ url = config["RequestURL"]
23
+ # print(f"{url=}")
24
+ method = config["RequestMethod"]
25
+ name = config["FileFormName"]
26
+ headers = config["Headers"]
27
+ key = config["URL"]
28
+
29
+ path = Path(file_name)
30
+ mime_type = get_type(path)
31
+ # print(f"{mime_type=}")
32
+
33
+ files = {name: (file_name, file_data, mime_type)}
34
+ r = session.request(method, url, headers=headers, files=files) # nosec
35
+ if not r.ok: # pragma: no cover
36
+ print(f"{r.status_code=}")
37
+ print(f"{r.text=}")
38
+ r.raise_for_status()
39
+
40
+ data = r.json()
41
+ # print(f"{data=}")
42
+
43
+ key_path = key.strip("{}").split(":")[1]
44
+ # print(f"{key_path=}")
45
+
46
+ jsonpath_expr = parse(key_path)
47
+ match = [match.value for match in jsonpath_expr.find(data)]
48
+ # print(f"{match=}")
49
+ return match[0]
50
+
51
+
52
+ def get_config(config_path: Optional[Path] = None, env_var: str = "", create_empty: bool = True):
53
+ """Parse a JSON config from Env or Path or return None"""
54
+ if env_var:
55
+ var = os.environ.get(env_var, None)
56
+ if var:
57
+ try:
58
+ return json.loads(var)
59
+ except JSONDecodeError:
60
+ pass
61
+ if not config_path:
62
+ return None
63
+ if not config_path.is_file():
64
+ if create_empty:
65
+ config_path.touch()
66
+ return None
67
+ try:
68
+ with open(config_path, "r") as f:
69
+ return json.loads(f.read())
70
+ except JSONDecodeError:
71
+ return None
72
+
73
+
74
+ def get_type(file_path: Path) -> str: # NOSONAR
75
+ """
76
+ Get MIME type from guess_type or by reading magic headers
77
+ https://en.wikipedia.org/wiki/List_of_file_signatures
78
+
79
+ Deprecated since version 3.13: Passing a file path instead of URL is soft deprecated. Use guess_file_type() for this.
80
+ https://docs.python.org/3/library/mimetypes.html#mimetypes.guess_type
81
+ """
82
+ mime_type, _ = mimetypes.guess_type(file_path, strict=False)
83
+ if mime_type:
84
+ return mime_type
85
+
86
+ if not file_path.is_file():
87
+ return "text/plain"
88
+
89
+ with open(file_path, "rb") as file:
90
+ chunk = file.read(512)
91
+
92
+ # print(f"chunk: {type(chunk)} - {chunk[:20]}")
93
+ # print(f"test chunk: {chunk[8:11]}")
94
+
95
+ if isinstance(chunk, str): # pragma: no cover
96
+ warnings.warn("This condition should not be True...", stacklevel=2)
97
+ return "text/plain"
98
+
99
+ # Images
100
+ if chunk[0:3] == b"\xff\xd8\xff" or chunk[6:10] in (b"JFIF", b"Exif"):
101
+ return "image/jpeg"
102
+ elif chunk.startswith(b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"):
103
+ return "image/png"
104
+ elif chunk.startswith(b"RIFF") and chunk[8:12] == b"WEBP":
105
+ return "image/webp"
106
+ elif chunk.startswith((b"\x47\x49\x46\x38\x37\x61", b"\x47\x49\x46\x38\x39\x61")):
107
+ return "image/gif"
108
+ elif chunk.startswith(b"\x66\x74\x79\x70\x68\x65\x69\x63\x66\x74\79\70\6d") or chunk[4:12] == b"ftypheic":
109
+ return "image/heic"
110
+ elif chunk.startswith(b"\x00\x00\x01\x00"):
111
+ return "image/ico"
112
+ elif chunk.startswith(b"II*") or chunk.startswith(b"II+") or chunk.startswith(b"MM"):
113
+ return "image/tiff"
114
+ elif chunk.startswith(b"BM"):
115
+ return "image/bmp"
116
+ elif chunk[4:12] == b"ftypavif":
117
+ return "image/avif"
118
+
119
+ # Video
120
+ elif chunk[4:12] == b"ftypisom" or chunk[4:12] == b"ftypMSNV" or chunk[4:12] == b"ftypmp42":
121
+ return "video/mp4"
122
+ elif chunk[3:11] in (b"\x66\x74\x79\x70\x4d\x53\x4e\x56", b"\x66\x74\x79\x70\x69\x73\x6f\x6d"):
123
+ return "video/mp4"
124
+ elif chunk.startswith(b"\x1a\x45\xdf\xa3"):
125
+ # https://www.loc.gov/preservation/digital/formats//fdd/fdd000342.shtml
126
+ return "video/x-matroska"
127
+ elif chunk.startswith(b"RIFF") and chunk[8:11] == b"AVI":
128
+ return "video/x-msvideo"
129
+ elif chunk.startswith(b"\x30\x26\xb2\x75\x8e\x66\xcf\x11\xa6\xd9\x00\xaa\x00\x62\xce\x6c"):
130
+ # https://www.loc.gov/preservation/digital/formats/fdd/fdd000091.shtml
131
+ return "video/x-ms-asf"
132
+ elif chunk.startswith(b"\x6d\x6f\x6f\x76") or chunk[4:10] == b"ftypqt":
133
+ # https://www.file-recovery.com/mov-signature-format.htm
134
+ return "video/quicktime"
135
+
136
+ # Audio
137
+ elif chunk.startswith((b"\xff\xfb", b"\xff\xfb", b"\xff\xfb", b"\x49\x44\x33")):
138
+ return "audio/mp3"
139
+ elif chunk.startswith(b"RIFF") and chunk[8:12] == b"WAVE":
140
+ return "audio/wav"
141
+ elif chunk.startswith(b"OggS"):
142
+ return "application/ogg"
143
+
144
+ # Fallback
145
+ try:
146
+ chunk.decode("utf-8")
147
+ return "text/plain"
148
+ except UnicodeDecodeError:
149
+ return "application/octet-stream"
@@ -0,0 +1,275 @@
1
+ import json
2
+ import os
3
+ import shutil
4
+ import sys
5
+ import tempfile
6
+ from json import JSONDecodeError
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import click
11
+ import pyperclip
12
+ import typer
13
+ from rich import print
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.table import Table
17
+ from typing_extensions import Annotated
18
+
19
+ from . import __doc__ as package_doc
20
+ from . import api, utils
21
+ from ._version import __version__
22
+
23
+
24
+ app = typer.Typer(
25
+ context_settings={"help_option_names": ["-h", "--help"]},
26
+ pretty_exceptions_show_locals=False,
27
+ add_completion=False,
28
+ )
29
+
30
+ console = Console(stderr=True)
31
+
32
+ state = {"verbose": False, "copy": True}
33
+
34
+
35
+ def vprint(*objects: Any, sep="\n", **kwargs):
36
+ if state.get("verbose"):
37
+ print(*objects, file=sys.stderr, sep=sep, **kwargs)
38
+
39
+
40
+ def copy_text(text: str):
41
+ if state.get("copy"):
42
+ try:
43
+ pyperclip.copy(text)
44
+ except pyperclip.PyperclipException as error:
45
+ vprint(f"[yellow bold]Clipboard Warning[/yellow bold]: {error}")
46
+
47
+
48
+ def get_config_path(name: str = "sharex-cli", create: bool = False) -> Path:
49
+ app_path = Path(typer.get_app_dir(name))
50
+ if not app_path.is_dir():
51
+ app_path.mkdir(parents=True)
52
+ config_path = app_path / "config.json"
53
+ if create:
54
+ config_path.touch()
55
+ return config_path
56
+
57
+
58
+ def update_config(config_path: Path, config_files: List[Path]):
59
+ console.rule("[bold green]ShareX CLI Configuration", style="cyan bold")
60
+ vprint(f"{config_path=}", f"{config_files=}")
61
+ user_config = None
62
+
63
+ if config_files and config_files[0].is_file():
64
+ vprint(f"{config_files[0]=}")
65
+ user_config = json.loads(config_files[0].read_text())
66
+ vprint(f"1 {user_config=}")
67
+
68
+ if not user_config:
69
+ print(
70
+ "\nDownload your servers ShareX Custom Uploader (*.sxcu) configuration, then:\n\n"
71
+ " - Type or Paste the config [cyan bold]file path[/cyan bold].\n"
72
+ " - Press [green bold]Enter[/green bold] to open a [cyan bold]text editor[/cyan bold].\n"
73
+ " - Or press [yellow bold]Ctrl+C[/yellow bold] to abort.\n"
74
+ )
75
+ input_file = Path(typer.prompt("File Path", default="", show_default=False))
76
+ vprint(f"{input_file=}")
77
+ if input_file.is_file():
78
+ user_config = json.loads(input_file.read_text())
79
+ vprint(f"2 {user_config=}")
80
+
81
+ if not user_config:
82
+ res: Optional[str] = click.edit()
83
+ vprint(f"{res=}")
84
+ if res:
85
+ user_config = json.loads(res)
86
+ vprint(f"3 {user_config=}")
87
+
88
+ if user_config and isinstance(user_config, dict):
89
+ config_path.write_text(json.dumps(user_config))
90
+ print(f"\n[green bold]Config Saved/Updated[/green bold]: [cyan bold]{config_path.absolute()}\n")
91
+ else:
92
+ raise click.ClickException("Error Saving Config")
93
+
94
+
95
+ def archive_dir(config, params: Dict[str, Any], dir_path: Path):
96
+ vprint(f"{params}", f"{dir_path=}", f"{os.getcwd()=}")
97
+ file_name = dir_path.name
98
+ vprint(f"{file_name=}")
99
+ if _name := params.get("_name"):
100
+ file_name = _name
101
+ if file_name.lower().endswith(".zip"):
102
+ file_name = file_name[:-4]
103
+ vprint(f"{file_name=}")
104
+ table = Table(title="Archive Details")
105
+ table.add_column("File Name", style="magenta bold", no_wrap=True)
106
+ table.add_column("Path", style="cyan bold")
107
+ table.add_row(file_name + ".zip", dir_path.absolute().as_posix())
108
+ console.print(table)
109
+ if not params.get("_yes"):
110
+ typer.confirm("Create and Upload Archive?", abort=True)
111
+
112
+ with tempfile.TemporaryDirectory() as d:
113
+ vprint(f"{d=}")
114
+ path = Path(d)
115
+ archive = path / file_name
116
+ vprint(f"{archive.absolute()=}")
117
+ with console.status(f"Creating Archive: [cyan bold]{archive.absolute()}"):
118
+ name = shutil.make_archive(archive.absolute().as_posix(), "zip", dir_path)
119
+ vprint(f"{name=}")
120
+ final_path = path / name
121
+ vprint(f"{final_path=}", f"{final_path.name=}")
122
+ size = utils.fmt_bytes(final_path.stat().st_size)
123
+ with console.status(
124
+ f"Uploading File: [green bold]{click.format_filename(final_path.name)}[/green bold] - [yellow bold]{size}"
125
+ ):
126
+ with open(final_path, "rb") as f:
127
+ url = api.upload_file(config, final_path.name, f)
128
+ print(url)
129
+ copy_text(url)
130
+ raise typer.Exit()
131
+
132
+
133
+ def version_callback(value: bool):
134
+ if value:
135
+ if package_doc:
136
+ print(package_doc.lstrip("\n"), file=sys.stderr)
137
+ print(f"[white bold]{__version__}")
138
+ raise typer.Exit()
139
+
140
+
141
+ @app.command()
142
+ def main( # NOSONAR
143
+ ctx: typer.Context,
144
+ files: Annotated[Optional[List[Path]], typer.Argument(help="Files or Directory...", exists=True)] = None,
145
+ _name: Annotated[
146
+ Optional[str], typer.Option("-n", "--name", metavar="", help="File Name (sent with upload).")
147
+ ] = None,
148
+ _glob: Annotated[
149
+ str, typer.Option("-g", "--glob", metavar="", help="Directory Files Glob (use ** to recurse).")
150
+ ] = "*",
151
+ _archive: Annotated[
152
+ bool, typer.Option("-a", "--archive", help="Directory Create Archive (no glob support).")
153
+ ] = False,
154
+ _yes: Annotated[
155
+ bool, typer.Option("-y", "--yes", metavar="", help="Answer YES to Prompts.", envvar="SHAREX_YES")
156
+ ] = False,
157
+ _copy: Annotated[
158
+ bool, typer.Option("--copy/--no-copy", "-c/-nc", help="Copy URL to Clipboard.", envvar="SHAREX_COPY")
159
+ ] = True,
160
+ _launch: Annotated[
161
+ bool,
162
+ typer.Option("--launch/--no-launch", "-l/-nl", help="Launch URL in Browser.", envvar="SHAREX_LAUNCH"),
163
+ ] = True,
164
+ _verbose: Annotated[
165
+ bool, typer.Option("-v", "--verbose", help="Verbose Output.", envvar="SHAREX_VERBOSE")
166
+ ] = False,
167
+ _config: Annotated[bool, typer.Option("-C", "--config", help="Update Configuration.")] = False,
168
+ _version: Annotated[
169
+ Optional[bool],
170
+ typer.Option("-V", "--version", callback=version_callback, help="Show Installed Version."),
171
+ ] = None,
172
+ ):
173
+ """
174
+ Files: A File, Multiple Files or a Directory.
175
+
176
+ Docs: https://cssnr.github.io/sharex-cli/
177
+ """
178
+ # vprint(f"{dir(ctx)=}")
179
+ vprint(f"{ctx.params=}")
180
+ state["verbose"], state["copy"] = _verbose, _copy
181
+ # state.update({"verbose": _verbose, "copy": _copy})
182
+ vprint(f"{state=}")
183
+
184
+ _files = files or []
185
+ vprint(f"{_files=}")
186
+ show_confirm = len(_files) > 1
187
+ vprint(f"{show_confirm=}")
188
+
189
+ config_path = get_config_path()
190
+ vprint(f"config_path: [cyan bold]{config_path.resolve()}")
191
+
192
+ config = api.get_config(config_path, "SHAREX_CONFIG")
193
+ vprint(f"config loaded: {bool(config)}")
194
+
195
+ vprint(f"{_config=}")
196
+ if not config or _config:
197
+ # Process Configuration
198
+ try:
199
+ update_config(config_path, _files)
200
+ raise typer.Exit()
201
+ except JSONDecodeError as error:
202
+ vprint(f"{error=}")
203
+ raise click.ClickException(f"Error Decoding Config: {error.msg}") # noqa: B904
204
+
205
+ tty = sys.stdin.isatty()
206
+ vprint(f"{tty=}")
207
+
208
+ if not tty or not _files:
209
+ # Process STDIN
210
+ file_name = _name or f"{utils.gen_rand(8)}.txt"
211
+ vprint(f"{file_name=}")
212
+ content = click.get_text_stream("stdin")
213
+ url = api.upload_file(config, file_name, content)
214
+ print(url)
215
+ copy_text(url)
216
+ if _launch:
217
+ typer.launch(url)
218
+ raise typer.Exit()
219
+
220
+ if len(_files) == 1 and _files[0].is_dir():
221
+ # Process Directory
222
+ vprint(f"{_archive=}", f"{_yes=}")
223
+ vprint(f"Processing Directory: [cyan bold]{_files[0].name}")
224
+ if _archive:
225
+ archive_dir(config, ctx.params, _files[0])
226
+ raise typer.Exit()
227
+ _files = [p for p in _files[0].glob(_glob.strip('"`')) if p.is_file()]
228
+ show_confirm = True
229
+ vprint(f"{_files=}")
230
+ else:
231
+ # Remove Directories
232
+ files, dirs = [], []
233
+ for fp in _files:
234
+ files.append(fp) if fp.is_file() else dirs.append(fp)
235
+ if dirs:
236
+ _files = files
237
+ text = "[yellow]" + "\n".join(p.absolute().as_posix() for p in dirs)
238
+ print(Panel.fit(text, title=f"Directories Removed: [white]{len(dirs)}", style="red", title_align="left"))
239
+ print("[yellow bold]Warning[/yellow bold]: Directories can be processed individually.\n", file=sys.stderr)
240
+
241
+ # Process Files
242
+ if not _files:
243
+ raise click.ClickException("No Files to Process...")
244
+ table = Table(title=f"Processing {len(_files)} File{utils.s(_files)}")
245
+ table.add_column("File Name", style="magenta bold", no_wrap=True)
246
+ table.add_column("Size", style="yellow bold")
247
+ table.add_column("Path", style="cyan bold")
248
+ for path in _files:
249
+ table.add_row(path.name, utils.fmt_bytes(path.stat().st_size), path.absolute().as_posix())
250
+ console.print(table)
251
+
252
+ vprint(f"{show_confirm=}")
253
+ if show_confirm and not _yes:
254
+ typer.confirm(f"Confirm Uploading {len(_files)} Files?", abort=True)
255
+
256
+ for i, file in enumerate(_files, 1):
257
+ if file.is_dir():
258
+ print(f"[yellow bold]Skipping directory[yellow bold]: [cyan bold]{file.absolute()}", file=sys.stderr)
259
+ continue
260
+ size = utils.fmt_bytes(file.stat().st_size)
261
+ with console.status(
262
+ f"Uploading {i}/{len(_files)}: [green bold]{click.format_filename(file.name)}[/green bold] - [yellow bold]{size}"
263
+ ):
264
+ file_name = _name if _name and len(_files) == 1 else file.name
265
+ with open(file, "rb") as f:
266
+ url = api.upload_file(config, file_name, f)
267
+ print(url)
268
+ copy_text(url)
269
+ if _launch and len(_files) == 1 and url:
270
+ typer.launch(url)
271
+ raise typer.Exit()
272
+
273
+
274
+ if __name__ == "__main__":
275
+ app()
File without changes
@@ -0,0 +1,19 @@
1
+ import secrets
2
+ import string
3
+
4
+
5
+ def s(to_len: list) -> str:
6
+ return "s" if len(to_len) > 1 else ""
7
+
8
+
9
+ def gen_rand(length: int = 4) -> str:
10
+ r = "".join(secrets.choice(string.ascii_letters) for _ in range(length))
11
+ return "".join(r)
12
+
13
+
14
+ def fmt_bytes(num: float, suffix: str = "B") -> str:
15
+ for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
16
+ if abs(num) < 1024.0:
17
+ return f"{num:3.1f} {unit}{suffix}"
18
+ num /= 1024.0
19
+ return f"{num:.1f} Yi{suffix}"