botgauge-cli 0.1.0__py3-none-any.whl

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,42 @@
1
+ """The ``botgauge`` command.
2
+
3
+ Owns the ``botgauge`` console script and its short alias, and dispatches each command to the
4
+ installed BotGauge component that registers it in the ``botgauge.commands`` entry-point group.
5
+ Standard library only.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from ._dispatch import main
10
+ from ._names import ENTRY_POINT_GROUP, RESERVED, command_names
11
+
12
+ #: Read from the installed metadata on first access (see ``__getattr__``); declared here for type checkers.
13
+ __version__: str
14
+ __all__ = ["ENTRY_POINT_GROUP", "RESERVED", "__version__", "command_names", "main"]
15
+
16
+
17
+ def __getattr__(name: str) -> str:
18
+ """Resolve ``__version__`` lazily from the installed distribution metadata.
19
+
20
+ The value is cached in the module globals after the first lookup.
21
+
22
+ Args:
23
+ name: The attribute being looked up on the module.
24
+
25
+ Returns:
26
+ The installed ``botgauge-cli`` version, or ``"0+unknown"`` when it is not installed.
27
+
28
+ Raises:
29
+ AttributeError: ``name`` is not ``__version__``.
30
+ """
31
+ # The version is defined once, in pyproject.toml, and read from the installed metadata the first
32
+ # time something asks for it. A source tree that was never installed reports "0+unknown".
33
+ if name == "__version__":
34
+ from importlib import metadata
35
+
36
+ try:
37
+ version = metadata.version("botgauge-cli")
38
+ except metadata.PackageNotFoundError:
39
+ version = "0+unknown"
40
+ globals()["__version__"] = version
41
+ return version
42
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,6 @@
1
+ """``python -m botgauge.cli`` runs the same dispatcher as the ``botgauge`` console script."""
2
+ import sys
3
+
4
+ from . import main
5
+
6
+ sys.exit(main())
@@ -0,0 +1,241 @@
1
+ """Dispatch ``botgauge <command>`` to the component that owns it.
2
+
3
+ Components register commands in the ``botgauge.commands`` entry-point group; this module only finds
4
+ and runs them. Rules, so a command never silently changes meaning:
5
+
6
+ * a reserved name (``_names.RESERVED``) runs only from its owner; another distribution registering it
7
+ is ignored and reported by ``--help``;
8
+ * a reserved name is resolved from the owner's metadata alone, never a scan of every installed
9
+ distribution, so ``botgauge instrument`` (which execs the application) stays cheap;
10
+ * an unreserved name registered twice is refused, naming every registering distribution; nothing
11
+ wins silently;
12
+ * a command whose owner is not installed says which distribution provides it;
13
+ * a component that fails to import fails only its own command, with a message naming it.
14
+
15
+ Exit codes: the command's own; 0 for help/version; 2 for usage errors (no, unknown, ambiguous,
16
+ unavailable or unloadable command).
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import sys
22
+ from importlib import metadata
23
+
24
+ from ._names import (
25
+ BUILTINS,
26
+ DISTRIBUTION,
27
+ ENTRY_POINT_GROUP,
28
+ PLATFORM,
29
+ RESERVED,
30
+ command_names,
31
+ install_hint,
32
+ )
33
+
34
+ _USAGE = "usage: {prog} <command> [args] ({prog} help for every installed command)\n"
35
+
36
+
37
+ def _prog() -> str:
38
+ """Return the console-script name the user invoked, or ``botgauge`` if it is not one of ours."""
39
+ invoked = os.path.basename(sys.argv[0] or "")
40
+ invoked = invoked[:-4] if invoked.lower().endswith(".exe") else invoked
41
+ return invoked if invoked in command_names() else "botgauge"
42
+
43
+
44
+ def _distribution(ep: metadata.EntryPoint) -> str:
45
+ """Return the name of the distribution that registered ``ep``, or a placeholder if unknown."""
46
+ return getattr(getattr(ep, "dist", None), "name", None) or "<unknown distribution>"
47
+
48
+
49
+ def _all_command_entry_points() -> dict[str, list[metadata.EntryPoint]]:
50
+ """Scan every installed distribution and group its ``botgauge.commands`` entry points by name."""
51
+ found: dict[str, list[metadata.EntryPoint]] = {}
52
+ for ep in metadata.entry_points(group=ENTRY_POINT_GROUP):
53
+ found.setdefault(ep.name, []).append(ep)
54
+ return found
55
+
56
+
57
+ def _owner_entry_point(name: str, owner: str) -> tuple[metadata.EntryPoint | None, str]:
58
+ """Find the owner's entry point for a reserved name, reading only the owner's metadata.
59
+
60
+ Args:
61
+ name: The reserved command name.
62
+ owner: The distribution that must provide ``name``.
63
+
64
+ Returns:
65
+ ``(entry_point, "")`` on success, or ``(None, problem)`` when the owner is not installed or
66
+ does not register ``name``.
67
+ """
68
+ try:
69
+ dist = metadata.distribution(owner)
70
+ except metadata.PackageNotFoundError:
71
+ return None, f"'{name}' is provided by {owner}, which is not installed ({install_hint(owner)})"
72
+ for ep in dist.entry_points:
73
+ if ep.group == ENTRY_POINT_GROUP and ep.name == name:
74
+ return ep, ""
75
+ return None, f"{owner} is installed but does not register '{name}' (a version mismatch?)"
76
+
77
+
78
+ def _resolve(name: str) -> tuple[metadata.EntryPoint | None, str]:
79
+ """Resolve a command name to the single entry point allowed to run it.
80
+
81
+ Args:
82
+ name: The command name typed after ``botgauge``.
83
+
84
+ Returns:
85
+ ``(entry_point, "")`` on success, or ``(None, problem)`` for an unknown, ambiguous or
86
+ unavailable command.
87
+ """
88
+ if name in RESERVED:
89
+ return _owner_entry_point(name, RESERVED[name])
90
+ candidates = _all_command_entry_points().get(name, [])
91
+ if not candidates:
92
+ return None, f"unknown command '{name}'"
93
+ if len(candidates) > 1:
94
+ owners = ", ".join(sorted(_distribution(ep) for ep in candidates))
95
+ return None, f"'{name}' is registered by more than one package ({owners}); refusing to guess"
96
+ return candidates[0], ""
97
+
98
+
99
+ def _run(name: str, rest: list[str], prog: str) -> int:
100
+ """Load and run one command, reporting resolution and import failures on stderr.
101
+
102
+ Args:
103
+ name: The command name.
104
+ rest: Arguments passed to the command's handler.
105
+ prog: The invoked program name, used in error messages.
106
+
107
+ Returns:
108
+ The handler's return value as an int (``None`` counts as 0), or 2 if the command could not be
109
+ resolved or loaded.
110
+ """
111
+ ep, problem = _resolve(name)
112
+ if ep is None:
113
+ print(f"{prog}: {problem}", file=sys.stderr)
114
+ print(f"Run '{prog} help' to list the installed commands.", file=sys.stderr)
115
+ return 2
116
+ try:
117
+ handler = ep.load()
118
+ except Exception as exc: # noqa: BLE001 - a broken component must not take the whole CLI down
119
+ print(f"{prog}: the '{name}' command (from {_distribution(ep)}) could not be loaded: "
120
+ f"{type(exc).__name__}: {exc}", file=sys.stderr)
121
+ return 2
122
+ result = handler(rest)
123
+ return int(result) if result is not None else 0
124
+
125
+
126
+ def _summary(ep: metadata.EntryPoint) -> str:
127
+ """Return the first line of the handler's docstring for ``help``; this imports the component.
128
+
129
+ For a command that ``help`` lists, the handler's docstring is therefore user-visible help
130
+ text: changing its first line changes the ``botgauge help`` output. (An unreserved name
131
+ registered more than once is not listed, and neither is a reserved name registered only by a
132
+ non-owner; a reserved name is listed from its owner's entry even when other distributions
133
+ also register it.)
134
+
135
+ Args:
136
+ ep: Entry point of the command whose summary is wanted.
137
+
138
+ Returns:
139
+ The summary line; an empty string for a handler without a docstring, or
140
+ ``(could not be loaded: <type>)`` when the component fails to import.
141
+ """
142
+ try:
143
+ doc = ep.load().__doc__ or ""
144
+ except Exception as exc: # noqa: BLE001 - list it as unavailable, keep listing the rest
145
+ return f"(could not be loaded: {type(exc).__name__})"
146
+ return doc.strip().splitlines()[0] if doc.strip() else ""
147
+
148
+
149
+ def _print_help(prog: str) -> None:
150
+ """Print every installed command by section, the reserved commands not installed, and ignored registrations."""
151
+ others = [n for n in command_names() if n != prog]
152
+ alias = f" (also: {', '.join(others)})" if others else ""
153
+ print(_USAGE.format(prog=prog).rstrip("\n") + alias)
154
+ found = _all_command_entry_points()
155
+ rows: dict[str, list[tuple[str, str, str]]] = {"Platform": [], "Groups": [], "Other": []}
156
+ ignored: list[str] = []
157
+ for name in BUILTINS:
158
+ rows["Platform"].append((name, "this help" if name == "help" else "installed versions", DISTRIBUTION))
159
+ for name, eps in sorted(found.items()):
160
+ owner = RESERVED.get(name)
161
+ if owner is not None:
162
+ ignored += [f"{_distribution(ep)} registers '{name}', which is reserved for {owner}"
163
+ for ep in eps if _distribution(ep) != owner]
164
+ eps = [ep for ep in eps if _distribution(ep) == owner]
165
+ if not eps:
166
+ continue
167
+ section = "Platform" if name in PLATFORM else "Groups"
168
+ elif len(eps) > 1:
169
+ owners = ", ".join(sorted(_distribution(ep) for ep in eps))
170
+ ignored.append(f"'{name}' is registered by {owners}; refused")
171
+ continue
172
+ else:
173
+ section = "Other"
174
+ rows[section].append((name, _summary(eps[0]), _distribution(eps[0])))
175
+ titles = {"Platform": "Commands", "Groups": f"Groups ({prog} <group> <verb>; {prog} <group> --help)",
176
+ "Other": "Other installed commands"}
177
+ for section, entries in rows.items():
178
+ if entries:
179
+ print(f"\n{titles[section]}:")
180
+ for name, summary, dist in entries:
181
+ print(f" {name:<12} {summary} [{dist}]")
182
+ installed = {name for name, eps in found.items() for ep in eps if _distribution(ep) == RESERVED.get(name)}
183
+ missing: dict[str, list[str]] = {}
184
+ for name, owner in RESERVED.items():
185
+ if name not in installed:
186
+ missing.setdefault(owner, []).append(name)
187
+ if missing:
188
+ print("\nNot installed:")
189
+ for owner, cmds in sorted(missing.items()):
190
+ print(f" {', '.join(cmds):<40} {install_hint(owner)}")
191
+ for line in ignored:
192
+ print(f"\nignored: {line}")
193
+
194
+
195
+ def _print_version() -> None:
196
+ """Print this distribution's version and that of every distribution registering a command."""
197
+ try:
198
+ own = metadata.version(DISTRIBUTION)
199
+ except metadata.PackageNotFoundError:
200
+ own = "(not installed)"
201
+ print(f"{DISTRIBUTION} {own}")
202
+ dists = sorted({_distribution(ep) for eps in _all_command_entry_points().values() for ep in eps})
203
+ for dist in dists:
204
+ try:
205
+ print(f" {dist} {metadata.version(dist)}")
206
+ except metadata.PackageNotFoundError:
207
+ print(f" {dist}")
208
+
209
+
210
+ def main(argv: list[str] | None = None) -> int:
211
+ """Dispatch one ``botgauge`` command.
212
+
213
+ Args:
214
+ argv: Command arguments, or None to use ``sys.argv[1:]``.
215
+
216
+ Returns:
217
+ The command's exit code (``help <command>`` returns that command's exit code for
218
+ ``--help``); 0 for help/version; 2 for a missing, unknown, ambiguous, uninstalled or
219
+ unloadable command. ``help`` followed by a built-in name (``help`` or ``version``) is
220
+ looked up like any other command name: it is an unknown command (2) unless a plugin
221
+ registers that name, in which case the plugin runs with ``--help``.
222
+
223
+ Raises:
224
+ SystemExit: A command's argument parser exits for help or invalid options.
225
+ Exception: Any other exception raised by a command handler propagates unchanged.
226
+ """
227
+ args = sys.argv[1:] if argv is None else list(argv)
228
+ prog = _prog()
229
+ if not args:
230
+ print(_USAGE.format(prog=prog), end="")
231
+ return 2
232
+ first, rest = args[0], args[1:]
233
+ if first in ("-h", "--help", "help"):
234
+ if first == "help" and rest:
235
+ return _run(rest[0], ["--help"], prog)
236
+ _print_help(prog)
237
+ return 0
238
+ if first in ("-V", "--version", "version"):
239
+ _print_version()
240
+ return 0
241
+ return _run(first, rest, prog)
botgauge/cli/_names.py ADDED
@@ -0,0 +1,78 @@
1
+ """Who owns which ``botgauge`` command.
2
+
3
+ Every top-level name a BotGauge distribution may register is listed here with its owner. The
4
+ dispatcher runs a reserved name only from its owner, so another package can never take over
5
+ ``botgauge instrument``, and resolving a reserved name reads only the owner's
6
+ metadata instead of scanning every installed distribution (``botgauge instrument`` stays cheap).
7
+ Third-party plugins may register other names; they never shadow these.
8
+
9
+ Adding a component's command is a reviewed change to this file, released with that component
10
+ (lockstep versions). A BotGauge distribution must not register a name that is not reserved for it
11
+ here.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from importlib import metadata
16
+
17
+ #: The entry-point group every component registers its commands under: ``name = "pkg.mod:fn"``,
18
+ #: where ``fn(argv: list[str]) -> int``.
19
+ ENTRY_POINT_GROUP = "botgauge.commands"
20
+
21
+ #: This distribution; its console scripts are the command names.
22
+ DISTRIBUTION = "botgauge-cli"
23
+
24
+ #: Built into the dispatcher itself.
25
+ BUILTINS = ("help", "version")
26
+
27
+ #: Platform verbs stay top-level; every other name is a noun group: ``botgauge <noun> <verb>``.
28
+ PLATFORM = ("instrument", "probe", "doctor", "setup", *BUILTINS)
29
+
30
+ #: name -> the distribution that must provide it.
31
+ RESERVED: dict[str, str] = {
32
+ "instrument": "botgauge-tracing",
33
+ "probe": "botgauge-tracing",
34
+ "doctor": "botgauge-tracing",
35
+ "setup": "botgauge-tracing",
36
+ }
37
+
38
+ #: Distributions published on PyPI. Only these get a `pip install` hint: an unpublished name is
39
+ #: either a 404 or, once someone else registers it, a third party's code. Add a distribution here in
40
+ #: the release that first publishes it.
41
+ PUBLISHED = frozenset({"botgauge", "botgauge-cli", "botgauge-tracing"})
42
+
43
+
44
+ def install_hint(owner: str) -> str:
45
+ """Return how to install ``owner``'s commands.
46
+
47
+ Args:
48
+ owner: A distribution name.
49
+
50
+ Returns:
51
+ ``"pip install <owner>"`` for a published distribution, otherwise a note that this release
52
+ does not publish it.
53
+ """
54
+ return f"pip install {owner}" if owner in PUBLISHED else "not published in this release"
55
+
56
+
57
+ _PRIMARY = "botgauge"
58
+
59
+
60
+ def command_names() -> tuple[str, ...]:
61
+ """Return the installed console-script names for this dispatcher, ``botgauge`` first.
62
+
63
+ Read from this distribution's own metadata, so the short alias is defined once, in
64
+ ``pyproject.toml``.
65
+
66
+ Returns:
67
+ ``botgauge`` followed by every other console script of ``botgauge-cli``, or
68
+ ``("botgauge",)`` when the metadata is unavailable (a source checkout run without
69
+ installing) or declares no console scripts.
70
+ """
71
+ try:
72
+ eps = metadata.distribution(DISTRIBUTION).entry_points
73
+ except metadata.PackageNotFoundError:
74
+ return (_PRIMARY,)
75
+ # Every console script of this distribution, whatever it points at: a script aimed at the wrong
76
+ # function must show up here (and fail the release gates), not silently drop out.
77
+ names = sorted(ep.name for ep in eps if ep.group == "console_scripts")
78
+ return (_PRIMARY, *(n for n in names if n != _PRIMARY)) if names else (_PRIMARY,)
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: botgauge-cli
3
+ Version: 0.1.0
4
+ Summary: The `botgauge` command: one entry point that dispatches to every installed BotGauge component.
5
+ Author-email: "BotGauge AI Inc." <support@botgauge.com>
6
+ License-Expression: FSL-1.1-ALv2
7
+ Project-URL: Homepage, https://www.botgauge.com/
8
+ Keywords: botgauge,cli
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Environment :: Console
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # botgauge-cli
24
+
25
+ The `botgauge` command. It owns the `botgauge` console script (plus a short alias, `botg`) and
26
+ dispatches each command to the installed BotGauge component that provides it:
27
+
28
+ ```bash
29
+ botgauge help # every installed command, and which package provides the rest
30
+ botgauge version # installed BotGauge versions
31
+ botgauge instrument python app.py # provided by botgauge-tracing
32
+ botgauge doctor # provided by botgauge-tracing
33
+ ```
34
+
35
+ Components register commands in the `botgauge.commands` entry-point group. Top-level names are
36
+ reserved per package (`src/botgauge/cli/_names.py`), so one package can never take over another's
37
+ command. Standard library only; every BotGauge component depends on it. You normally get it through
38
+ `pip install botgauge` rather than installing it directly.
@@ -0,0 +1,10 @@
1
+ botgauge/cli/__init__.py,sha256=a4_oVNm2VKBQaHuNDQQ05C-6hHqt_4PdoONlvba9KHU,1579
2
+ botgauge/cli/__main__.py,sha256=YgvCeaA_NXPq8B-R_ArNqlOVse0w-12As4qjSIOJj1w,143
3
+ botgauge/cli/_dispatch.py,sha256=A4-y1ePaihfHD8v4v_TaGWrG0I3trduXtUeFbyN6TFU,10161
4
+ botgauge/cli/_names.py,sha256=rIO6hlZDDzUOAJGbEon-38ZzeQRsCNOAgaOK_wcFrX0,3160
5
+ botgauge_cli-0.1.0.dist-info/licenses/LICENSE,sha256=EplDjGiYxLsggvWIv03TjBXk_gABXU88NMWtl_T26cU,3748
6
+ botgauge_cli-0.1.0.dist-info/METADATA,sha256=ByNii7uEBR8P3-wxANjkUSdunVFboicEycJPopHnkhU,1697
7
+ botgauge_cli-0.1.0.dist-info/WHEEL,sha256=a-udL2m4V_6Yn8b-uKzWb12RjRxwwMo6ohYFM24tpaY,91
8
+ botgauge_cli-0.1.0.dist-info/entry_points.txt,sha256=GiQBZ1kSGAPZa2GRMJgJVlSuxpOpChYhteLBtRR9xK0,72
9
+ botgauge_cli-0.1.0.dist-info/top_level.txt,sha256=qr2kZrd0gkelTuQTeCyA68V8ewK04QkEynqd-So9t88,9
10
+ botgauge_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ botg = botgauge.cli:main
3
+ botgauge = botgauge.cli:main
@@ -0,0 +1,105 @@
1
+ # Functional Source License, Version 1.1, ALv2 Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-ALv2
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 BotGauge AI Inc.
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the Apache License, Version 2.0 that is effective on the second anniversary of
91
+ the date we make the Software available. On or after that date, you may use the
92
+ Software under the Apache License, Version 2.0, in which case the following
93
+ will apply:
94
+
95
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
96
+ this file except in compliance with the License.
97
+
98
+ You may obtain a copy of the License at
99
+
100
+ http://www.apache.org/licenses/LICENSE-2.0
101
+
102
+ Unless required by applicable law or agreed to in writing, software distributed
103
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
+ specific language governing permissions and limitations under the License.
@@ -0,0 +1 @@
1
+ botgauge