wellspin 0.1.0__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.
- wellspin-0.1.0/.gitignore +4 -0
- wellspin-0.1.0/LICENSE +21 -0
- wellspin-0.1.0/PKG-INFO +89 -0
- wellspin-0.1.0/README.md +52 -0
- wellspin-0.1.0/pyproject.toml +31 -0
- wellspin-0.1.0/wellspin/__init__.py +27 -0
- wellspin-0.1.0/wellspin/_cli.py +214 -0
wellspin-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wellspin LLC, Yevhen Reuka and Dmytro Kazarov
|
|
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.
|
wellspin-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: wellspin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Prints https://cal.com/wellspin/30min?layout=mobile and opens it in your default browser.
|
|
5
|
+
Author: Wellspin LLC
|
|
6
|
+
Maintainer: Yevhen Reuka, Dmytro Kazarov
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 Wellspin LLC, Yevhen Reuka and Dmytro Kazarov
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Keywords: browser,cli,link,open
|
|
30
|
+
Classifier: Environment :: Console
|
|
31
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Topic :: Utilities
|
|
35
|
+
Requires-Python: >=3.9
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# wellspin
|
|
39
|
+
|
|
40
|
+
Prints a link and opens it in your default browser. That is the whole program.
|
|
41
|
+
|
|
42
|
+
uvx wellspin
|
|
43
|
+
|
|
44
|
+
It writes the URL to stdout and asks the operating system to open it. Whether
|
|
45
|
+
the page then loads is between your browser and the network — this command
|
|
46
|
+
cannot see that and does not claim it.
|
|
47
|
+
|
|
48
|
+
## The link
|
|
49
|
+
|
|
50
|
+
https://cal.com/wellspin/30min?layout=mobile
|
|
51
|
+
|
|
52
|
+
## Options
|
|
53
|
+
|
|
54
|
+
| Command | What it does |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `uvx wellspin` | print the URL and open it |
|
|
57
|
+
| `uvx wellspin --url` | print the URL only, open nothing |
|
|
58
|
+
| `uvx wellspin --help` | usage |
|
|
59
|
+
| `uvx wellspin --version` | the version of this package |
|
|
60
|
+
|
|
61
|
+
`pipx run wellspin` behaves the same way, and `pip install wellspin`
|
|
62
|
+
puts `wellspin` on your PATH if you would rather keep it.
|
|
63
|
+
|
|
64
|
+
In CI the URL is printed and no browser is launched.
|
|
65
|
+
|
|
66
|
+
## Getting the current link
|
|
67
|
+
|
|
68
|
+
The URL is baked in when the package is published, so a copy already on your
|
|
69
|
+
machine keeps whatever link it shipped with. To be sure you have the newest:
|
|
70
|
+
|
|
71
|
+
uvx wellspin@latest
|
|
72
|
+
|
|
73
|
+
A changed link means a new version of this package.
|
|
74
|
+
|
|
75
|
+
## Exit codes
|
|
76
|
+
|
|
77
|
+
| Code | Meaning |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| 0 | the URL was printed — including when the browser could not be launched |
|
|
80
|
+
| 1 | this package has no usable URL configured |
|
|
81
|
+
| 2 | bad arguments |
|
|
82
|
+
|
|
83
|
+
## Dependencies
|
|
84
|
+
|
|
85
|
+
None. `webbrowser` is part of the Python standard library.
|
|
86
|
+
|
|
87
|
+
## Licence
|
|
88
|
+
|
|
89
|
+
MIT.
|
wellspin-0.1.0/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# wellspin
|
|
2
|
+
|
|
3
|
+
Prints a link and opens it in your default browser. That is the whole program.
|
|
4
|
+
|
|
5
|
+
uvx wellspin
|
|
6
|
+
|
|
7
|
+
It writes the URL to stdout and asks the operating system to open it. Whether
|
|
8
|
+
the page then loads is between your browser and the network — this command
|
|
9
|
+
cannot see that and does not claim it.
|
|
10
|
+
|
|
11
|
+
## The link
|
|
12
|
+
|
|
13
|
+
https://cal.com/wellspin/30min?layout=mobile
|
|
14
|
+
|
|
15
|
+
## Options
|
|
16
|
+
|
|
17
|
+
| Command | What it does |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `uvx wellspin` | print the URL and open it |
|
|
20
|
+
| `uvx wellspin --url` | print the URL only, open nothing |
|
|
21
|
+
| `uvx wellspin --help` | usage |
|
|
22
|
+
| `uvx wellspin --version` | the version of this package |
|
|
23
|
+
|
|
24
|
+
`pipx run wellspin` behaves the same way, and `pip install wellspin`
|
|
25
|
+
puts `wellspin` on your PATH if you would rather keep it.
|
|
26
|
+
|
|
27
|
+
In CI the URL is printed and no browser is launched.
|
|
28
|
+
|
|
29
|
+
## Getting the current link
|
|
30
|
+
|
|
31
|
+
The URL is baked in when the package is published, so a copy already on your
|
|
32
|
+
machine keeps whatever link it shipped with. To be sure you have the newest:
|
|
33
|
+
|
|
34
|
+
uvx wellspin@latest
|
|
35
|
+
|
|
36
|
+
A changed link means a new version of this package.
|
|
37
|
+
|
|
38
|
+
## Exit codes
|
|
39
|
+
|
|
40
|
+
| Code | Meaning |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| 0 | the URL was printed — including when the browser could not be launched |
|
|
43
|
+
| 1 | this package has no usable URL configured |
|
|
44
|
+
| 2 | bad arguments |
|
|
45
|
+
|
|
46
|
+
## Dependencies
|
|
47
|
+
|
|
48
|
+
None. `webbrowser` is part of the Python standard library.
|
|
49
|
+
|
|
50
|
+
## Licence
|
|
51
|
+
|
|
52
|
+
MIT.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wellspin"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Prints https://cal.com/wellspin/30min?layout=mobile and opens it in your default browser."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
keywords = ["cli", "open", "browser", "link"]
|
|
13
|
+
authors = [{ name = "Wellspin LLC" }]
|
|
14
|
+
maintainers = [{ name = "Yevhen Reuka" }, { name = "Dmytro Kazarov" }]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: End Users/Desktop",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Utilities",
|
|
21
|
+
]
|
|
22
|
+
dependencies = []
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
wellspin = "wellspin:main"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["wellspin"]
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.sdist]
|
|
31
|
+
include = ["wellspin", "README.md", "LICENSE"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""wellspin — print a link and open it in your default browser."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version as _version
|
|
4
|
+
|
|
5
|
+
from ._cli import main as _main
|
|
6
|
+
|
|
7
|
+
NAME = "wellspin"
|
|
8
|
+
URL = "https://cal.com/wellspin/30min?layout=mobile"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _installed_version() -> str:
|
|
12
|
+
try:
|
|
13
|
+
return _version(NAME)
|
|
14
|
+
except PackageNotFoundError: # running from a checkout rather than an install
|
|
15
|
+
return "0+unknown"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> int:
|
|
19
|
+
"""Console entry point. Returns the exit code."""
|
|
20
|
+
return _main(NAME, URL, _installed_version())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _console() -> None:
|
|
24
|
+
raise SystemExit(main())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = ["NAME", "URL", "main"]
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Shared behaviour for every one of these commands.
|
|
2
|
+
|
|
3
|
+
A deliberate mirror of src/run.js — same flags, same exit codes, same refusal
|
|
4
|
+
to claim more than it knows. Kept free of I/O so the tests can drive it without
|
|
5
|
+
opening tabs, and copied verbatim into each published package.
|
|
6
|
+
|
|
7
|
+
Nothing outside the standard library is used: webbrowser ships with Python, so
|
|
8
|
+
these packages have no dependencies at all.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import webbrowser
|
|
16
|
+
from typing import Callable, Iterable, Optional, Sequence, Tuple
|
|
17
|
+
from urllib.parse import urlsplit
|
|
18
|
+
|
|
19
|
+
EXIT_OK = 0
|
|
20
|
+
EXIT_CONFIG = 1
|
|
21
|
+
EXIT_USAGE = 2
|
|
22
|
+
|
|
23
|
+
# Values that mean "nobody filled this in yet". Rejected rather than quietly
|
|
24
|
+
# replaced with some default page.
|
|
25
|
+
_PLACEHOLDERS = {
|
|
26
|
+
"none",
|
|
27
|
+
"null",
|
|
28
|
+
"tbd",
|
|
29
|
+
"todo",
|
|
30
|
+
"placeholder",
|
|
31
|
+
"changeme",
|
|
32
|
+
"change-me",
|
|
33
|
+
"replaceme",
|
|
34
|
+
"replace-me",
|
|
35
|
+
"http://",
|
|
36
|
+
"https://",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_CI_SIGNALS = (
|
|
40
|
+
"CI",
|
|
41
|
+
"CONTINUOUS_INTEGRATION",
|
|
42
|
+
"GITHUB_ACTIONS",
|
|
43
|
+
"GITLAB_CI",
|
|
44
|
+
"CIRCLECI",
|
|
45
|
+
"TRAVIS",
|
|
46
|
+
"BUILDKITE",
|
|
47
|
+
"DRONE",
|
|
48
|
+
"APPVEYOR",
|
|
49
|
+
"TEAMCITY_VERSION",
|
|
50
|
+
"TF_BUILD",
|
|
51
|
+
"JENKINS_URL",
|
|
52
|
+
"CODEBUILD_BUILD_ID",
|
|
53
|
+
"NETLIFY",
|
|
54
|
+
"VERCEL",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
_KNOWN_FLAGS = ("--help", "--version", "--url")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def detect_ci(env) -> Optional[str]:
|
|
61
|
+
"""Name the variable that identified a CI runner, or None.
|
|
62
|
+
|
|
63
|
+
CI=false and CI=0 count as "not CI" — some shells export it blank.
|
|
64
|
+
"""
|
|
65
|
+
for key in _CI_SIGNALS:
|
|
66
|
+
value = env.get(key)
|
|
67
|
+
if value is None:
|
|
68
|
+
continue
|
|
69
|
+
text = str(value).strip().lower()
|
|
70
|
+
if text in ("", "false", "0"):
|
|
71
|
+
continue
|
|
72
|
+
return key
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def read_url(raw) -> Tuple[bool, str]:
|
|
77
|
+
"""Check a configured URL without rewriting it.
|
|
78
|
+
|
|
79
|
+
Returns (ok, url_or_reason). The URL comes back trimmed but otherwise
|
|
80
|
+
exactly as configured, so path, query and fragment survive untouched.
|
|
81
|
+
"""
|
|
82
|
+
if raw is None:
|
|
83
|
+
return False, "no URL is configured for this package"
|
|
84
|
+
if not isinstance(raw, str):
|
|
85
|
+
return False, "URL must be a string, got %s" % type(raw).__name__
|
|
86
|
+
|
|
87
|
+
url = raw.strip()
|
|
88
|
+
if not url:
|
|
89
|
+
return False, "the configured URL is empty"
|
|
90
|
+
if url.lower() in _PLACEHOLDERS or "<" in url or ">" in url:
|
|
91
|
+
return False, "the configured URL is still a placeholder: %s" % url
|
|
92
|
+
|
|
93
|
+
# urlsplit rather than a hand-rolled pattern: an earlier regex here left
|
|
94
|
+
# "@" out of the host class, so a link carrying credentials fell through to
|
|
95
|
+
# the generic "not a valid URL" message instead of the specific one. The
|
|
96
|
+
# standard parser knows the grammar; matching it by hand does not.
|
|
97
|
+
try:
|
|
98
|
+
parts = urlsplit(url)
|
|
99
|
+
except ValueError as error:
|
|
100
|
+
return False, "the configured URL is not a valid URL: %s (%s)" % (url, error)
|
|
101
|
+
|
|
102
|
+
if parts.scheme != "https":
|
|
103
|
+
return False, "the URL must use https: %s" % url
|
|
104
|
+
try:
|
|
105
|
+
if parts.username or parts.password:
|
|
106
|
+
return False, "the URL must not carry credentials"
|
|
107
|
+
host = parts.hostname
|
|
108
|
+
except ValueError as error:
|
|
109
|
+
return False, "the configured URL is not a valid URL: %s (%s)" % (url, error)
|
|
110
|
+
|
|
111
|
+
if not host:
|
|
112
|
+
return False, "the URL has no host: %s" % url
|
|
113
|
+
|
|
114
|
+
return True, url
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def help_text(name: str) -> str:
|
|
118
|
+
return "\n".join(
|
|
119
|
+
(
|
|
120
|
+
"%s — print a link and open it in your default browser." % name,
|
|
121
|
+
"",
|
|
122
|
+
"Usage",
|
|
123
|
+
" uvx %s print the URL and open it" % name,
|
|
124
|
+
" uvx %s --url print the URL only" % name,
|
|
125
|
+
" uvx %s --help show this help" % name,
|
|
126
|
+
" uvx %s --version show the version of this package" % name,
|
|
127
|
+
"",
|
|
128
|
+
"pipx run %s works the same way." % name,
|
|
129
|
+
"",
|
|
130
|
+
"The URL is fixed when the package is published. To be sure you have the",
|
|
131
|
+
"newest one, ask for it explicitly: uvx %s@latest" % name,
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def run(
|
|
137
|
+
argv: Sequence[str],
|
|
138
|
+
name: str,
|
|
139
|
+
version: str,
|
|
140
|
+
url,
|
|
141
|
+
open_url: Callable[[str], bool],
|
|
142
|
+
stdout: Callable[[str], None],
|
|
143
|
+
stderr: Callable[[str], None],
|
|
144
|
+
env,
|
|
145
|
+
) -> int:
|
|
146
|
+
"""The whole program. Returns the process exit code."""
|
|
147
|
+
unknown: Iterable[str] = [a for a in argv if a not in _KNOWN_FLAGS]
|
|
148
|
+
unknown = list(unknown)
|
|
149
|
+
if unknown:
|
|
150
|
+
stderr("%s: unknown argument: %s\nTry: %s --help\n" % (name, unknown[0], name))
|
|
151
|
+
return EXIT_USAGE
|
|
152
|
+
|
|
153
|
+
# One thing at a time: --url --version has no single sensible answer.
|
|
154
|
+
flags = list(dict.fromkeys(argv))
|
|
155
|
+
if len(flags) > 1:
|
|
156
|
+
stderr("%s: use one option at a time, got %s\nTry: %s --help\n" % (name, " ".join(flags), name))
|
|
157
|
+
return EXIT_USAGE
|
|
158
|
+
|
|
159
|
+
flag = flags[0] if flags else None
|
|
160
|
+
|
|
161
|
+
# Both answer before the URL is looked at, so they still work on a package
|
|
162
|
+
# whose link has not been filled in yet.
|
|
163
|
+
if flag == "--help":
|
|
164
|
+
stdout(help_text(name) + "\n")
|
|
165
|
+
return EXIT_OK
|
|
166
|
+
if flag == "--version":
|
|
167
|
+
stdout("%s\n" % version)
|
|
168
|
+
return EXIT_OK
|
|
169
|
+
|
|
170
|
+
ok, value = read_url(url)
|
|
171
|
+
if not ok:
|
|
172
|
+
stderr("%s: %s\n" % (name, value))
|
|
173
|
+
return EXIT_CONFIG
|
|
174
|
+
|
|
175
|
+
if flag == "--url":
|
|
176
|
+
stdout("%s\n" % value)
|
|
177
|
+
return EXIT_OK
|
|
178
|
+
|
|
179
|
+
# The link reaches the user first, so it is theirs whatever happens next.
|
|
180
|
+
stdout("%s\n" % value)
|
|
181
|
+
|
|
182
|
+
ci = detect_ci(env)
|
|
183
|
+
if ci:
|
|
184
|
+
stderr("%s: %s is set, not opening a browser.\n" % (name, ci))
|
|
185
|
+
return EXIT_OK
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
launched = open_url(value)
|
|
189
|
+
except Exception as error: # noqa: BLE001 - any failure is the same to us
|
|
190
|
+
launched = False
|
|
191
|
+
detail = str(error) or error.__class__.__name__
|
|
192
|
+
stderr("%s: could not launch a browser (%s). Open the URL above yourself.\n" % (name, detail))
|
|
193
|
+
else:
|
|
194
|
+
if not launched:
|
|
195
|
+
stderr("%s: could not launch a browser. Open the URL above yourself.\n" % name)
|
|
196
|
+
|
|
197
|
+
# Handing over the link is the job. Whether the page then loaded is not
|
|
198
|
+
# something this process can observe, so it is not claimed either way.
|
|
199
|
+
return EXIT_OK
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def main(name: str, url, version: str) -> int:
|
|
203
|
+
return run(
|
|
204
|
+
argv=sys.argv[1:],
|
|
205
|
+
name=name,
|
|
206
|
+
version=version,
|
|
207
|
+
url=url,
|
|
208
|
+
# webbrowser builds the platform command itself and never goes through
|
|
209
|
+
# a shell, so a link carrying quotes or semicolons stays a link.
|
|
210
|
+
open_url=lambda target: webbrowser.open(target, new=2),
|
|
211
|
+
stdout=sys.stdout.write,
|
|
212
|
+
stderr=sys.stderr.write,
|
|
213
|
+
env=os.environ,
|
|
214
|
+
)
|