crowddrop-sdk 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.
- crowddrop_sdk-0.1.0/PKG-INFO +122 -0
- crowddrop_sdk-0.1.0/README.md +110 -0
- crowddrop_sdk-0.1.0/crowddrop_sdk/__init__.py +5 -0
- crowddrop_sdk-0.1.0/crowddrop_sdk/cloud_brain/__init__.py +13 -0
- crowddrop_sdk-0.1.0/crowddrop_sdk/cloud_brain/drone/__init__.py +3 -0
- crowddrop_sdk-0.1.0/crowddrop_sdk/cloud_brain/drone/channels.py +88 -0
- crowddrop_sdk-0.1.0/crowddrop_sdk/cloud_brain/drone/edge_agent.py +193 -0
- crowddrop_sdk-0.1.0/crowddrop_sdk/cloud_brain/events.py +89 -0
- crowddrop_sdk-0.1.0/crowddrop_sdk.egg-info/PKG-INFO +122 -0
- crowddrop_sdk-0.1.0/crowddrop_sdk.egg-info/SOURCES.txt +13 -0
- crowddrop_sdk-0.1.0/crowddrop_sdk.egg-info/dependency_links.txt +1 -0
- crowddrop_sdk-0.1.0/crowddrop_sdk.egg-info/requires.txt +5 -0
- crowddrop_sdk-0.1.0/crowddrop_sdk.egg-info/top_level.txt +1 -0
- crowddrop_sdk-0.1.0/pyproject.toml +21 -0
- crowddrop_sdk-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crowddrop-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CrowdDrop's SDK for embodied-agent hardware. This release covers cloud_brain: the LLM stays in the cloud, the device is a thin actuator/sensor bridge over Pub/Sub.
|
|
5
|
+
Project-URL: Repository, https://github.com/crowddrop-ai/crowddrop_ai_agents
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Provides-Extra: cloud-brain
|
|
9
|
+
Requires-Dist: crowddrop-pubsub-sdk<1.0.0,>=0.2.0; extra == "cloud-brain"
|
|
10
|
+
Requires-Dist: requests<3.0.0,>=2.28.0; extra == "cloud-brain"
|
|
11
|
+
Requires-Dist: google-auth<3.0.0,>=2.29.0; extra == "cloud-brain"
|
|
12
|
+
|
|
13
|
+
# crowddrop-sdk
|
|
14
|
+
|
|
15
|
+
CrowdDrop's SDK for connecting embodied-agent hardware to the CrowdDrop
|
|
16
|
+
platform. It splits into two architecturally different scenarios:
|
|
17
|
+
|
|
18
|
+
- **`cloud_brain`** (this release) - the LLM/tool-calling brain stays in
|
|
19
|
+
CrowdDrop's cloud backend; your device is a thin actuator/sensor bridge
|
|
20
|
+
over Google Cloud Pub/Sub. This is the right choice if your hardware can't
|
|
21
|
+
run an LLM locally but can run a real Python process.
|
|
22
|
+
- **`edge_brain`** (planned, not yet released) - the LLM itself runs on your
|
|
23
|
+
device. A different SDK surface entirely, for hardware with real local
|
|
24
|
+
inference capacity.
|
|
25
|
+
|
|
26
|
+
If you're building a companion-computer-class device (e.g. Raspberry-Pi
|
|
27
|
+
class, full Linux + Python) that receives commands and reports telemetry,
|
|
28
|
+
you want the `cloud-brain` extra:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install "crowddrop-sdk[cloud-brain]"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This pulls in `crowddrop-pubsub-sdk` (CrowdDrop's Pub/Sub transport library)
|
|
35
|
+
as its only dependency - nothing else, so it stays light on constrained
|
|
36
|
+
hardware. The base `crowddrop-sdk` install (no extra) has no dependencies at
|
|
37
|
+
all.
|
|
38
|
+
|
|
39
|
+
## Getting credentials
|
|
40
|
+
|
|
41
|
+
There's no self-service signup. CrowdDrop issues one **agent device key**
|
|
42
|
+
per physical device, tied to the persona it embodies, and hands it to you
|
|
43
|
+
out-of-band along with the URL of the backend's token-vending endpoint. If
|
|
44
|
+
you don't have both yet, ask whoever set up your CrowdDrop persona - there's
|
|
45
|
+
no dashboard to generate one yourself. Your device never handles a raw GCP
|
|
46
|
+
service-account key file: it trades its device key for a short-lived GCP
|
|
47
|
+
access token by calling the token-vending endpoint (see
|
|
48
|
+
`cloud_brain/drone/edge_agent.py`'s `fetch_gcp_access_token`), and refreshes
|
|
49
|
+
that token automatically as it nears expiry.
|
|
50
|
+
|
|
51
|
+
## API reference (`cloud_brain`)
|
|
52
|
+
|
|
53
|
+
- **`crowddrop_sdk.cloud_brain.events.DRONE_COMMANDS`** - the eight
|
|
54
|
+
supported movement primitives, each a fixed pulse with no
|
|
55
|
+
duration/distance parameter: `take_off`, `land`, `forward`, `backward`,
|
|
56
|
+
`strafe_left`, `strafe_right`, `turn_left`, `turn_right`.
|
|
57
|
+
- **`DroneCommandEvent(drone_id, command, issued_at, sequence)`** - what you
|
|
58
|
+
receive, one per command.
|
|
59
|
+
- **`DroneTelemetryEvent(drone_id, latitude, longitude, heading,
|
|
60
|
+
battery_level, reported_at, sequence)`** - what you publish back.
|
|
61
|
+
- **`crowddrop_sdk.cloud_brain.drone.channels.DroneCommandSubscriber`** -
|
|
62
|
+
pull-based subscriber for commands (you always initiate the connection
|
|
63
|
+
outward - nothing is ever pushed to your device).
|
|
64
|
+
- **`crowddrop_sdk.cloud_brain.drone.channels.DroneTelemetryPublisher`** -
|
|
65
|
+
publisher for telemetry, with a `publish_telemetry(drone_id, latitude,
|
|
66
|
+
longitude, heading, battery_level)` convenience method.
|
|
67
|
+
- **Two integration points you implement** - `handle_command(event)` (map
|
|
68
|
+
each command to your real flight-controller call) and
|
|
69
|
+
`read_battery_and_gps()` (return a real sensor reading). Both are stand-ins
|
|
70
|
+
in the example below; this SDK doesn't know your hardware's API.
|
|
71
|
+
|
|
72
|
+
## Quickstart
|
|
73
|
+
|
|
74
|
+
See [`cloud_brain/drone/README.md`](crowddrop_sdk/cloud_brain/drone/README.md)
|
|
75
|
+
for a runnable example (`edge_agent.py`) and the exact env vars it needs.
|
|
76
|
+
|
|
77
|
+
## Releasing (publishing a new version to PyPI)
|
|
78
|
+
|
|
79
|
+
Releases are tag-triggered via `.github/workflows/publish-crowddrop-sdk.yml`,
|
|
80
|
+
using PyPI's **Trusted Publishing** (OIDC) — no API token is stored as a
|
|
81
|
+
GitHub secret.
|
|
82
|
+
|
|
83
|
+
1. Bump `version` in `crowddrop_sdk/pyproject.toml`.
|
|
84
|
+
2. Commit that change (on a branch, via the normal PR flow).
|
|
85
|
+
3. Once merged, tag the merge commit and push the tag:
|
|
86
|
+
```bash
|
|
87
|
+
git tag crowddrop-sdk-v<version> # e.g. crowddrop-sdk-v0.1.1
|
|
88
|
+
git push origin crowddrop-sdk-v<version>
|
|
89
|
+
```
|
|
90
|
+
The tag push is what fires the workflow — it builds `crowddrop_sdk/` and
|
|
91
|
+
uploads it to [pypi.org/project/crowddrop-sdk](https://pypi.org/project/crowddrop-sdk/).
|
|
92
|
+
No other trigger publishes this package.
|
|
93
|
+
|
|
94
|
+
If `cloud-brain`'s dependency on `crowddrop-pubsub-sdk` (see `pyproject.toml`)
|
|
95
|
+
needs bumping too, release `pubsub_sdk` first — see its own README's
|
|
96
|
+
"Releasing" section, same mechanism, separate workflow/tag prefix
|
|
97
|
+
(`pubsub-sdk-v*`).
|
|
98
|
+
|
|
99
|
+
**One-time setup, not yet done as of this writing — needed before the first
|
|
100
|
+
tag push, and again only if this ever moves to a different PyPI
|
|
101
|
+
account/org:**
|
|
102
|
+
- Register a **pending publisher** for `crowddrop-sdk` at
|
|
103
|
+
https://pypi.org/manage/account/publishing/ — this can be done before the
|
|
104
|
+
PyPI project exists, so it covers the *first-ever* release too, not just
|
|
105
|
+
subsequent ones. Fill in: PyPI project name `crowddrop-sdk`, repo owner
|
|
106
|
+
`crowddrop-ai`, repo name `crowddrop_ai_agents`, workflow filename
|
|
107
|
+
`publish-crowddrop-sdk.yml`, environment name `pypi`. Requires a PyPI
|
|
108
|
+
account with 2FA enabled — no API token to generate or store.
|
|
109
|
+
- The `pypi` GitHub Environment referenced by the workflow is created
|
|
110
|
+
automatically the first time the workflow runs against it; create it
|
|
111
|
+
manually in this repo's Settings → Environments beforehand only if you
|
|
112
|
+
want a required-reviewer protection rule (so a tag push pauses for human
|
|
113
|
+
approval before it actually publishes).
|
|
114
|
+
- `../scripts/publish_python_packages.sh` (manual `build` + `twine upload`)
|
|
115
|
+
is kept as a fallback/local-dry-run tool only — with a pending publisher
|
|
116
|
+
registered, it's no longer needed even for the first release.
|
|
117
|
+
|
|
118
|
+
Versioning is manual — nothing cross-checks the tag against
|
|
119
|
+
`pyproject.toml`'s `version`. Bump the file first, commit, *then* tag that
|
|
120
|
+
exact commit; tagging a commit whose `pyproject.toml` still has an
|
|
121
|
+
already-published version will fail the upload (PyPI rejects re-uploading an
|
|
122
|
+
existing version).
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# crowddrop-sdk
|
|
2
|
+
|
|
3
|
+
CrowdDrop's SDK for connecting embodied-agent hardware to the CrowdDrop
|
|
4
|
+
platform. It splits into two architecturally different scenarios:
|
|
5
|
+
|
|
6
|
+
- **`cloud_brain`** (this release) - the LLM/tool-calling brain stays in
|
|
7
|
+
CrowdDrop's cloud backend; your device is a thin actuator/sensor bridge
|
|
8
|
+
over Google Cloud Pub/Sub. This is the right choice if your hardware can't
|
|
9
|
+
run an LLM locally but can run a real Python process.
|
|
10
|
+
- **`edge_brain`** (planned, not yet released) - the LLM itself runs on your
|
|
11
|
+
device. A different SDK surface entirely, for hardware with real local
|
|
12
|
+
inference capacity.
|
|
13
|
+
|
|
14
|
+
If you're building a companion-computer-class device (e.g. Raspberry-Pi
|
|
15
|
+
class, full Linux + Python) that receives commands and reports telemetry,
|
|
16
|
+
you want the `cloud-brain` extra:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install "crowddrop-sdk[cloud-brain]"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This pulls in `crowddrop-pubsub-sdk` (CrowdDrop's Pub/Sub transport library)
|
|
23
|
+
as its only dependency - nothing else, so it stays light on constrained
|
|
24
|
+
hardware. The base `crowddrop-sdk` install (no extra) has no dependencies at
|
|
25
|
+
all.
|
|
26
|
+
|
|
27
|
+
## Getting credentials
|
|
28
|
+
|
|
29
|
+
There's no self-service signup. CrowdDrop issues one **agent device key**
|
|
30
|
+
per physical device, tied to the persona it embodies, and hands it to you
|
|
31
|
+
out-of-band along with the URL of the backend's token-vending endpoint. If
|
|
32
|
+
you don't have both yet, ask whoever set up your CrowdDrop persona - there's
|
|
33
|
+
no dashboard to generate one yourself. Your device never handles a raw GCP
|
|
34
|
+
service-account key file: it trades its device key for a short-lived GCP
|
|
35
|
+
access token by calling the token-vending endpoint (see
|
|
36
|
+
`cloud_brain/drone/edge_agent.py`'s `fetch_gcp_access_token`), and refreshes
|
|
37
|
+
that token automatically as it nears expiry.
|
|
38
|
+
|
|
39
|
+
## API reference (`cloud_brain`)
|
|
40
|
+
|
|
41
|
+
- **`crowddrop_sdk.cloud_brain.events.DRONE_COMMANDS`** - the eight
|
|
42
|
+
supported movement primitives, each a fixed pulse with no
|
|
43
|
+
duration/distance parameter: `take_off`, `land`, `forward`, `backward`,
|
|
44
|
+
`strafe_left`, `strafe_right`, `turn_left`, `turn_right`.
|
|
45
|
+
- **`DroneCommandEvent(drone_id, command, issued_at, sequence)`** - what you
|
|
46
|
+
receive, one per command.
|
|
47
|
+
- **`DroneTelemetryEvent(drone_id, latitude, longitude, heading,
|
|
48
|
+
battery_level, reported_at, sequence)`** - what you publish back.
|
|
49
|
+
- **`crowddrop_sdk.cloud_brain.drone.channels.DroneCommandSubscriber`** -
|
|
50
|
+
pull-based subscriber for commands (you always initiate the connection
|
|
51
|
+
outward - nothing is ever pushed to your device).
|
|
52
|
+
- **`crowddrop_sdk.cloud_brain.drone.channels.DroneTelemetryPublisher`** -
|
|
53
|
+
publisher for telemetry, with a `publish_telemetry(drone_id, latitude,
|
|
54
|
+
longitude, heading, battery_level)` convenience method.
|
|
55
|
+
- **Two integration points you implement** - `handle_command(event)` (map
|
|
56
|
+
each command to your real flight-controller call) and
|
|
57
|
+
`read_battery_and_gps()` (return a real sensor reading). Both are stand-ins
|
|
58
|
+
in the example below; this SDK doesn't know your hardware's API.
|
|
59
|
+
|
|
60
|
+
## Quickstart
|
|
61
|
+
|
|
62
|
+
See [`cloud_brain/drone/README.md`](crowddrop_sdk/cloud_brain/drone/README.md)
|
|
63
|
+
for a runnable example (`edge_agent.py`) and the exact env vars it needs.
|
|
64
|
+
|
|
65
|
+
## Releasing (publishing a new version to PyPI)
|
|
66
|
+
|
|
67
|
+
Releases are tag-triggered via `.github/workflows/publish-crowddrop-sdk.yml`,
|
|
68
|
+
using PyPI's **Trusted Publishing** (OIDC) — no API token is stored as a
|
|
69
|
+
GitHub secret.
|
|
70
|
+
|
|
71
|
+
1. Bump `version` in `crowddrop_sdk/pyproject.toml`.
|
|
72
|
+
2. Commit that change (on a branch, via the normal PR flow).
|
|
73
|
+
3. Once merged, tag the merge commit and push the tag:
|
|
74
|
+
```bash
|
|
75
|
+
git tag crowddrop-sdk-v<version> # e.g. crowddrop-sdk-v0.1.1
|
|
76
|
+
git push origin crowddrop-sdk-v<version>
|
|
77
|
+
```
|
|
78
|
+
The tag push is what fires the workflow — it builds `crowddrop_sdk/` and
|
|
79
|
+
uploads it to [pypi.org/project/crowddrop-sdk](https://pypi.org/project/crowddrop-sdk/).
|
|
80
|
+
No other trigger publishes this package.
|
|
81
|
+
|
|
82
|
+
If `cloud-brain`'s dependency on `crowddrop-pubsub-sdk` (see `pyproject.toml`)
|
|
83
|
+
needs bumping too, release `pubsub_sdk` first — see its own README's
|
|
84
|
+
"Releasing" section, same mechanism, separate workflow/tag prefix
|
|
85
|
+
(`pubsub-sdk-v*`).
|
|
86
|
+
|
|
87
|
+
**One-time setup, not yet done as of this writing — needed before the first
|
|
88
|
+
tag push, and again only if this ever moves to a different PyPI
|
|
89
|
+
account/org:**
|
|
90
|
+
- Register a **pending publisher** for `crowddrop-sdk` at
|
|
91
|
+
https://pypi.org/manage/account/publishing/ — this can be done before the
|
|
92
|
+
PyPI project exists, so it covers the *first-ever* release too, not just
|
|
93
|
+
subsequent ones. Fill in: PyPI project name `crowddrop-sdk`, repo owner
|
|
94
|
+
`crowddrop-ai`, repo name `crowddrop_ai_agents`, workflow filename
|
|
95
|
+
`publish-crowddrop-sdk.yml`, environment name `pypi`. Requires a PyPI
|
|
96
|
+
account with 2FA enabled — no API token to generate or store.
|
|
97
|
+
- The `pypi` GitHub Environment referenced by the workflow is created
|
|
98
|
+
automatically the first time the workflow runs against it; create it
|
|
99
|
+
manually in this repo's Settings → Environments beforehand only if you
|
|
100
|
+
want a required-reviewer protection rule (so a tag push pauses for human
|
|
101
|
+
approval before it actually publishes).
|
|
102
|
+
- `../scripts/publish_python_packages.sh` (manual `build` + `twine upload`)
|
|
103
|
+
is kept as a fallback/local-dry-run tool only — with a pending publisher
|
|
104
|
+
registered, it's no longer needed even for the first release.
|
|
105
|
+
|
|
106
|
+
Versioning is manual — nothing cross-checks the tag against
|
|
107
|
+
`pyproject.toml`'s `version`. Bump the file first, commit, *then* tag that
|
|
108
|
+
exact commit; tagging a commit whose `pyproject.toml` still has an
|
|
109
|
+
already-published version will fail the upload (PyPI rejects re-uploading an
|
|
110
|
+
existing version).
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"""crowddrop-sdk: CrowdDrop's SDK for embodied-agent hardware. See
|
|
2
|
+
crowddrop_sdk.cloud_brain for this release's cloud-brain scenario (import
|
|
3
|
+
that submodule directly - the base install here declares no dependencies, so
|
|
4
|
+
nothing cloud_brain-specific is re-exported at this top level).
|
|
5
|
+
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""cloud_brain: the LLM/tool-calling brain stays in the cloud, the device is
|
|
2
|
+
a thin actuator/sensor bridge over Google Cloud Pub/Sub. This submodule ships
|
|
3
|
+
the shared wire contract (events.py) and the drone's own client classes
|
|
4
|
+
(drone/channels.py) - the backend's equivalents live outside this published
|
|
5
|
+
package, in the crowddrop_ai_agents repo itself.
|
|
6
|
+
"""
|
|
7
|
+
from .events import DRONE_COMMANDS, DroneCommandEvent, DroneTelemetryEvent
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"DRONE_COMMANDS",
|
|
11
|
+
"DroneCommandEvent",
|
|
12
|
+
"DroneTelemetryEvent",
|
|
13
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""The physical drone's own pub/sub classes - imported only by edge_agent.py
|
|
2
|
+
(and whatever real edge code eventually replaces/extends it). The backend's
|
|
3
|
+
equivalents (DroneCommandPublisher, DroneTelemetrySubscriber) live in
|
|
4
|
+
fastapi/app/first_citizen_drone/channels.py instead, outside this published
|
|
5
|
+
package - nothing outside that repo ever uses them.
|
|
6
|
+
|
|
7
|
+
Both classes are thin GenericPublisher/GenericSubscriber subclasses
|
|
8
|
+
(pubsub_sdk.client) - a subclass fixes the topic/subscription ids and message
|
|
9
|
+
type; everything else (client construction, degrade-gracefully behavior,
|
|
10
|
+
emulator-only auto-create, ack/nack) lives there once.
|
|
11
|
+
"""
|
|
12
|
+
import datetime
|
|
13
|
+
import itertools
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from pubsub_sdk import PubSubConfig
|
|
19
|
+
from pubsub_sdk.client import GenericPublisher, GenericSubscriber
|
|
20
|
+
|
|
21
|
+
from ..events import DroneCommandEvent, DroneTelemetryEvent
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
DRONE_COMMAND_TOPIC_ID_ENV = "DRONE_COMMAND_TOPIC_ID"
|
|
26
|
+
DRONE_COMMAND_SUBSCRIPTION_ID_ENV = "DRONE_COMMAND_SUBSCRIPTION_ID"
|
|
27
|
+
DRONE_TELEMETRY_TOPIC_ID_ENV = "DRONE_TELEMETRY_TOPIC_ID"
|
|
28
|
+
|
|
29
|
+
DEFAULT_DRONE_COMMAND_TOPIC_ID = "drone-command-events"
|
|
30
|
+
DEFAULT_DRONE_COMMAND_SUBSCRIPTION_ID = "drone-command-events-drone"
|
|
31
|
+
DEFAULT_DRONE_TELEMETRY_TOPIC_ID = "drone-telemetry-events"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DroneCommandSubscriber(GenericSubscriber[DroneCommandEvent]):
|
|
35
|
+
"""The drone consumes commands. Pull-based (pull_forever), matching
|
|
36
|
+
StepEventSubscriber's existing pattern in this org's backend - the drone
|
|
37
|
+
always initiates the connection outward, never accepts inbound (it's a
|
|
38
|
+
mobile, likely-NATed device with no static IP).
|
|
39
|
+
|
|
40
|
+
One fixed subscription (always_self_provision=False - provisioned ahead
|
|
41
|
+
of time by infra against real GCP, auto-created only against the
|
|
42
|
+
emulator), since exactly one physical drone is in scope today. drone_id
|
|
43
|
+
still rides as a message attribute (see events.py) so a second physical
|
|
44
|
+
drone can later get its own filter_expr-scoped subscription without a
|
|
45
|
+
wire-format change.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, config: Optional[PubSubConfig] = None):
|
|
49
|
+
cfg = config or PubSubConfig.from_env()
|
|
50
|
+
topic_id = os.environ.get(DRONE_COMMAND_TOPIC_ID_ENV, DEFAULT_DRONE_COMMAND_TOPIC_ID)
|
|
51
|
+
subscription_id = os.environ.get(DRONE_COMMAND_SUBSCRIPTION_ID_ENV, DEFAULT_DRONE_COMMAND_SUBSCRIPTION_ID)
|
|
52
|
+
super().__init__(
|
|
53
|
+
config=cfg,
|
|
54
|
+
topic_id=topic_id,
|
|
55
|
+
subscription_id=subscription_id,
|
|
56
|
+
message_cls=DroneCommandEvent,
|
|
57
|
+
label="DroneCommandSubscriber",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class DroneTelemetryPublisher(GenericPublisher[DroneTelemetryEvent]):
|
|
62
|
+
"""The drone publishes telemetry - real GPS/battery/heading readings, not
|
|
63
|
+
simulated and not a placeholder."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, config: Optional[PubSubConfig] = None):
|
|
66
|
+
cfg = config or PubSubConfig.from_env()
|
|
67
|
+
topic_id = os.environ.get(DRONE_TELEMETRY_TOPIC_ID_ENV, DEFAULT_DRONE_TELEMETRY_TOPIC_ID)
|
|
68
|
+
super().__init__(config=cfg, topic_id=topic_id, label="DroneTelemetryPublisher")
|
|
69
|
+
self._sequence = itertools.count(start=1)
|
|
70
|
+
|
|
71
|
+
def publish_telemetry(
|
|
72
|
+
self,
|
|
73
|
+
drone_id: str,
|
|
74
|
+
latitude: float,
|
|
75
|
+
longitude: float,
|
|
76
|
+
heading: float,
|
|
77
|
+
battery_level: float,
|
|
78
|
+
) -> bool:
|
|
79
|
+
event = DroneTelemetryEvent(
|
|
80
|
+
drone_id=drone_id,
|
|
81
|
+
latitude=latitude,
|
|
82
|
+
longitude=longitude,
|
|
83
|
+
heading=heading,
|
|
84
|
+
battery_level=battery_level,
|
|
85
|
+
reported_at=datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
86
|
+
sequence=next(self._sequence),
|
|
87
|
+
)
|
|
88
|
+
return self.publish(event)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Runnable example for the physical drone's companion computer. Shows the
|
|
2
|
+
full loop: fetch a short-lived GCP access token (never a key file - see the
|
|
3
|
+
README next to this file), receive commands, drive the flight controller,
|
|
4
|
+
and report telemetry back.
|
|
5
|
+
|
|
6
|
+
Two integration points are clearly marked below (handle_command,
|
|
7
|
+
read_battery_and_gps) - this file does not know the real flight-controller
|
|
8
|
+
API and doesn't guess it. Run it as-is against a stand-in (or with
|
|
9
|
+
PUBSUB_EMULATOR_HOST set, against the emulator) to see the wiring work end
|
|
10
|
+
to end before a real flight controller is plugged in.
|
|
11
|
+
"""
|
|
12
|
+
import datetime
|
|
13
|
+
import functools
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import threading
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
import google.auth.credentials
|
|
20
|
+
import google.auth.transport.requests
|
|
21
|
+
import requests
|
|
22
|
+
from google.auth import _helpers
|
|
23
|
+
from google.cloud import pubsub_v1
|
|
24
|
+
|
|
25
|
+
from .channels import DroneCommandSubscriber, DroneTelemetryPublisher
|
|
26
|
+
from ..events import DRONE_COMMANDS, DroneCommandEvent
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
DRONE_ID_ENV = "DRONE_ID"
|
|
31
|
+
DRONE_DEVICE_KEY_ENV = "DRONE_DEVICE_KEY"
|
|
32
|
+
BACKEND_TOKEN_VENDING_URL_ENV = "BACKEND_TOKEN_VENDING_URL"
|
|
33
|
+
|
|
34
|
+
TELEMETRY_HEARTBEAT_SECONDS = 10
|
|
35
|
+
GCP_TOKEN_REFRESH_MARGIN_SECONDS = 300
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def fetch_gcp_access_token(device_key: str, backend_url: str) -> dict:
|
|
39
|
+
"""POSTs to the backend's token-vending endpoint with the drone's one
|
|
40
|
+
agent device key and returns {"access_token": ..., "expires_in_seconds":
|
|
41
|
+
...}. Never a GCP service-account key file - see this package's own
|
|
42
|
+
README."""
|
|
43
|
+
response = requests.post(
|
|
44
|
+
backend_url,
|
|
45
|
+
headers={"X-Agent-Device-Key": device_key},
|
|
46
|
+
timeout=10,
|
|
47
|
+
)
|
|
48
|
+
response.raise_for_status()
|
|
49
|
+
return response.json()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _DeviceKeyCredentials(google.auth.credentials.Credentials):
|
|
53
|
+
"""A google-auth Credentials implementation whose refresh() calls back
|
|
54
|
+
into this backend's token-vending endpoint with the drone's one agent
|
|
55
|
+
device key, instead of the usual service-account/user OAuth flow - the
|
|
56
|
+
drone never handles a GCP service-account key file. The pubsub_v1 client
|
|
57
|
+
machinery calls refresh() automatically on the next new RPC once the
|
|
58
|
+
current token is near expiry; _run_token_refresh below additionally
|
|
59
|
+
refreshes proactively, since a long-lived streaming pull may not open a
|
|
60
|
+
new RPC before expiry on its own."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, device_key: str, backend_url: str):
|
|
63
|
+
super().__init__()
|
|
64
|
+
self._device_key = device_key
|
|
65
|
+
self._backend_url = backend_url
|
|
66
|
+
|
|
67
|
+
def refresh(self, request) -> None:
|
|
68
|
+
token_response = fetch_gcp_access_token(self._device_key, self._backend_url)
|
|
69
|
+
self.token = token_response["access_token"]
|
|
70
|
+
self.expiry = _helpers.utcnow() + datetime.timedelta(
|
|
71
|
+
seconds=token_response.get("expires_in_seconds", 3600)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _apply_device_key_credentials(device_key: str, backend_url: str) -> "_DeviceKeyCredentials":
|
|
76
|
+
"""Makes every PublisherClient()/SubscriberClient() construction in this
|
|
77
|
+
process (they're always called with no arguments, relying on ADC - see
|
|
78
|
+
pubsub_sdk.client.GenericPublisher/GenericSubscriber) use these
|
|
79
|
+
credentials instead - the same monkeypatch technique
|
|
80
|
+
scripts/pubsub_smoke_test.py already uses for a plain
|
|
81
|
+
`gcloud auth print-access-token`, just backed by a refreshable
|
|
82
|
+
Credentials object here instead of a fixed token. Returns the
|
|
83
|
+
credentials object so the caller can also refresh it proactively (see
|
|
84
|
+
_run_token_refresh) - the pubsub client's own lazy on-RPC refresh isn't
|
|
85
|
+
guaranteed to fire mid-stream on a long-lived streaming pull."""
|
|
86
|
+
creds = _DeviceKeyCredentials(device_key, backend_url)
|
|
87
|
+
pubsub_v1.PublisherClient = functools.partial(pubsub_v1.PublisherClient, credentials=creds)
|
|
88
|
+
pubsub_v1.SubscriberClient = functools.partial(pubsub_v1.SubscriberClient, credentials=creds)
|
|
89
|
+
return creds
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _run_token_refresh(creds: "_DeviceKeyCredentials", stop_event: threading.Event) -> None:
|
|
93
|
+
"""Proactively refreshes creds before they expire, rather than relying
|
|
94
|
+
solely on the pubsub client's lazy on-RPC refresh - belt-and-suspenders
|
|
95
|
+
for DroneCommandSubscriber's long-lived streaming pull, which may not
|
|
96
|
+
reopen (and thus re-authenticate) before the vended token expires."""
|
|
97
|
+
request = google.auth.transport.requests.Request()
|
|
98
|
+
while not stop_event.is_set():
|
|
99
|
+
creds.refresh(request)
|
|
100
|
+
sleep_seconds = max((creds.expiry - _helpers.utcnow()).total_seconds() - GCP_TOKEN_REFRESH_MARGIN_SECONDS, 1)
|
|
101
|
+
stop_event.wait(sleep_seconds)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def handle_command(event: DroneCommandEvent) -> None:
|
|
105
|
+
"""command -> flight-controller-call dispatch. This is the integration
|
|
106
|
+
point - wire up the real flight controller here, one branch per command
|
|
107
|
+
in DRONE_COMMANDS."""
|
|
108
|
+
if event.command not in DRONE_COMMANDS:
|
|
109
|
+
logger.error("handle_command: unknown command '%s', ignoring.", event.command)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
logger.info("handle_command: executing '%s' (sequence=%s)", event.command, event.sequence)
|
|
113
|
+
|
|
114
|
+
raise NotImplementedError(f"integration point: wire up the real flight controller here ({event.command})")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class SensorReading:
|
|
119
|
+
"""A raw sensor reading, distinct from DroneTelemetryEvent - this is just
|
|
120
|
+
what the hardware measured, not yet stamped with drone_id/reported_at/
|
|
121
|
+
sequence (those are publish_telemetry's job)."""
|
|
122
|
+
|
|
123
|
+
latitude: float
|
|
124
|
+
longitude: float
|
|
125
|
+
heading: float
|
|
126
|
+
battery_level: float
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def read_battery_and_gps() -> SensorReading:
|
|
130
|
+
"""Returns a real GPS/battery/heading reading. Stand-in below returns a
|
|
131
|
+
fixed reading, for anyone testing without hardware in hand - wire up the
|
|
132
|
+
real sensors here."""
|
|
133
|
+
return SensorReading(latitude=0.0, longitude=0.0, heading=0.0, battery_level=100.0)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _publish_telemetry_now(publisher: DroneTelemetryPublisher, drone_id: str) -> None:
|
|
137
|
+
reading = read_battery_and_gps()
|
|
138
|
+
publisher.publish_telemetry(
|
|
139
|
+
drone_id=drone_id,
|
|
140
|
+
latitude=reading.latitude,
|
|
141
|
+
longitude=reading.longitude,
|
|
142
|
+
heading=reading.heading,
|
|
143
|
+
battery_level=reading.battery_level,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _run_telemetry_heartbeat(publisher: DroneTelemetryPublisher, drone_id: str, stop_event: threading.Event) -> None:
|
|
148
|
+
while not stop_event.is_set():
|
|
149
|
+
_publish_telemetry_now(publisher, drone_id)
|
|
150
|
+
stop_event.wait(TELEMETRY_HEARTBEAT_SECONDS)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def main() -> None:
|
|
154
|
+
logging.basicConfig(level=logging.INFO)
|
|
155
|
+
|
|
156
|
+
drone_id = os.environ[DRONE_ID_ENV]
|
|
157
|
+
device_key = os.environ.get(DRONE_DEVICE_KEY_ENV)
|
|
158
|
+
backend_url = os.environ.get(BACKEND_TOKEN_VENDING_URL_ENV)
|
|
159
|
+
|
|
160
|
+
stop_event = threading.Event()
|
|
161
|
+
|
|
162
|
+
if device_key and backend_url:
|
|
163
|
+
creds = _apply_device_key_credentials(device_key, backend_url)
|
|
164
|
+
refresh_thread = threading.Thread(target=_run_token_refresh, args=(creds, stop_event), daemon=True)
|
|
165
|
+
refresh_thread.start()
|
|
166
|
+
else:
|
|
167
|
+
logger.info(
|
|
168
|
+
"%s/%s not set - assuming ambient credentials (e.g. PUBSUB_EMULATOR_HOST for local testing).",
|
|
169
|
+
DRONE_DEVICE_KEY_ENV,
|
|
170
|
+
BACKEND_TOKEN_VENDING_URL_ENV,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
command_subscriber = DroneCommandSubscriber()
|
|
174
|
+
telemetry_publisher = DroneTelemetryPublisher()
|
|
175
|
+
|
|
176
|
+
def _on_command(event: DroneCommandEvent) -> None:
|
|
177
|
+
handle_command(event)
|
|
178
|
+
_publish_telemetry_now(telemetry_publisher, drone_id)
|
|
179
|
+
|
|
180
|
+
heartbeat_thread = threading.Thread(
|
|
181
|
+
target=_run_telemetry_heartbeat, args=(telemetry_publisher, drone_id, stop_event), daemon=True
|
|
182
|
+
)
|
|
183
|
+
heartbeat_thread.start()
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
command_subscriber.pull_forever(_on_command)
|
|
187
|
+
finally:
|
|
188
|
+
stop_event.set()
|
|
189
|
+
command_subscriber.stop()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
main()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""DroneCommandEvent / DroneTelemetryEvent: the wire contract for CrowdDrop's
|
|
2
|
+
cloud-brain scenario - the LLM/tool-calling brain stays in the cloud, the
|
|
3
|
+
physical device is a thin actuator/sensor bridge over Pub/Sub. Both the
|
|
4
|
+
backend's own channel classes (fastapi/app/first_citizen_drone/channels.py,
|
|
5
|
+
not published) and the drone's (cloud_brain/drone/channels.py, this module's
|
|
6
|
+
sibling) import these two event types - the one thing genuinely shared
|
|
7
|
+
between the two sides of the wire.
|
|
8
|
+
|
|
9
|
+
Field names and the to_pubsub_message()/from_pubsub_message() shape mirror
|
|
10
|
+
pubsub_sdk's StepEvent (JSON body + string attributes for cheap server-side
|
|
11
|
+
filtering without deserializing) so a developer already familiar with that
|
|
12
|
+
pattern in the CrowdDrop backend recognizes this one immediately.
|
|
13
|
+
"""
|
|
14
|
+
import json
|
|
15
|
+
from dataclasses import asdict, dataclass
|
|
16
|
+
from typing import Dict, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
DRONE_COMMANDS = (
|
|
19
|
+
"take_off",
|
|
20
|
+
"land",
|
|
21
|
+
"forward",
|
|
22
|
+
"backward",
|
|
23
|
+
"strafe_left",
|
|
24
|
+
"strafe_right",
|
|
25
|
+
"turn_left",
|
|
26
|
+
"turn_right",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class DroneCommandEvent:
|
|
32
|
+
"""One fixed-pulse movement primitive, backend -> drone. No duration/
|
|
33
|
+
distance field on any of the eight commands - take-off always rises to
|
|
34
|
+
the same default altitude, "a lot" of lateral movement is expressed as
|
|
35
|
+
repeated messages, not a bigger number."""
|
|
36
|
+
|
|
37
|
+
drone_id: str
|
|
38
|
+
command: str
|
|
39
|
+
issued_at: str
|
|
40
|
+
sequence: Optional[int] = None
|
|
41
|
+
|
|
42
|
+
def to_pubsub_message(self) -> Tuple[bytes, Dict[str, str]]:
|
|
43
|
+
body = asdict(self)
|
|
44
|
+
data = json.dumps(body).encode("utf-8")
|
|
45
|
+
attributes = {"drone_id": self.drone_id, "command": self.command}
|
|
46
|
+
return data, attributes
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_pubsub_message(cls, data: bytes, attributes: Optional[Dict[str, str]] = None) -> "DroneCommandEvent":
|
|
50
|
+
body = json.loads(data.decode("utf-8"))
|
|
51
|
+
return cls(
|
|
52
|
+
drone_id=body["drone_id"],
|
|
53
|
+
command=body["command"],
|
|
54
|
+
issued_at=body["issued_at"],
|
|
55
|
+
sequence=body.get("sequence"),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class DroneTelemetryEvent:
|
|
61
|
+
"""One real GPS/battery/heading reading, drone -> backend. Real, not
|
|
62
|
+
simulated and not a placeholder."""
|
|
63
|
+
|
|
64
|
+
drone_id: str
|
|
65
|
+
latitude: float
|
|
66
|
+
longitude: float
|
|
67
|
+
heading: float
|
|
68
|
+
battery_level: float
|
|
69
|
+
reported_at: str
|
|
70
|
+
sequence: Optional[int] = None
|
|
71
|
+
|
|
72
|
+
def to_pubsub_message(self) -> Tuple[bytes, Dict[str, str]]:
|
|
73
|
+
body = asdict(self)
|
|
74
|
+
data = json.dumps(body).encode("utf-8")
|
|
75
|
+
attributes = {"drone_id": self.drone_id}
|
|
76
|
+
return data, attributes
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_pubsub_message(cls, data: bytes, attributes: Optional[Dict[str, str]] = None) -> "DroneTelemetryEvent":
|
|
80
|
+
body = json.loads(data.decode("utf-8"))
|
|
81
|
+
return cls(
|
|
82
|
+
drone_id=body["drone_id"],
|
|
83
|
+
latitude=body["latitude"],
|
|
84
|
+
longitude=body["longitude"],
|
|
85
|
+
heading=body["heading"],
|
|
86
|
+
battery_level=body["battery_level"],
|
|
87
|
+
reported_at=body["reported_at"],
|
|
88
|
+
sequence=body.get("sequence"),
|
|
89
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crowddrop-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CrowdDrop's SDK for embodied-agent hardware. This release covers cloud_brain: the LLM stays in the cloud, the device is a thin actuator/sensor bridge over Pub/Sub.
|
|
5
|
+
Project-URL: Repository, https://github.com/crowddrop-ai/crowddrop_ai_agents
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Provides-Extra: cloud-brain
|
|
9
|
+
Requires-Dist: crowddrop-pubsub-sdk<1.0.0,>=0.2.0; extra == "cloud-brain"
|
|
10
|
+
Requires-Dist: requests<3.0.0,>=2.28.0; extra == "cloud-brain"
|
|
11
|
+
Requires-Dist: google-auth<3.0.0,>=2.29.0; extra == "cloud-brain"
|
|
12
|
+
|
|
13
|
+
# crowddrop-sdk
|
|
14
|
+
|
|
15
|
+
CrowdDrop's SDK for connecting embodied-agent hardware to the CrowdDrop
|
|
16
|
+
platform. It splits into two architecturally different scenarios:
|
|
17
|
+
|
|
18
|
+
- **`cloud_brain`** (this release) - the LLM/tool-calling brain stays in
|
|
19
|
+
CrowdDrop's cloud backend; your device is a thin actuator/sensor bridge
|
|
20
|
+
over Google Cloud Pub/Sub. This is the right choice if your hardware can't
|
|
21
|
+
run an LLM locally but can run a real Python process.
|
|
22
|
+
- **`edge_brain`** (planned, not yet released) - the LLM itself runs on your
|
|
23
|
+
device. A different SDK surface entirely, for hardware with real local
|
|
24
|
+
inference capacity.
|
|
25
|
+
|
|
26
|
+
If you're building a companion-computer-class device (e.g. Raspberry-Pi
|
|
27
|
+
class, full Linux + Python) that receives commands and reports telemetry,
|
|
28
|
+
you want the `cloud-brain` extra:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install "crowddrop-sdk[cloud-brain]"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This pulls in `crowddrop-pubsub-sdk` (CrowdDrop's Pub/Sub transport library)
|
|
35
|
+
as its only dependency - nothing else, so it stays light on constrained
|
|
36
|
+
hardware. The base `crowddrop-sdk` install (no extra) has no dependencies at
|
|
37
|
+
all.
|
|
38
|
+
|
|
39
|
+
## Getting credentials
|
|
40
|
+
|
|
41
|
+
There's no self-service signup. CrowdDrop issues one **agent device key**
|
|
42
|
+
per physical device, tied to the persona it embodies, and hands it to you
|
|
43
|
+
out-of-band along with the URL of the backend's token-vending endpoint. If
|
|
44
|
+
you don't have both yet, ask whoever set up your CrowdDrop persona - there's
|
|
45
|
+
no dashboard to generate one yourself. Your device never handles a raw GCP
|
|
46
|
+
service-account key file: it trades its device key for a short-lived GCP
|
|
47
|
+
access token by calling the token-vending endpoint (see
|
|
48
|
+
`cloud_brain/drone/edge_agent.py`'s `fetch_gcp_access_token`), and refreshes
|
|
49
|
+
that token automatically as it nears expiry.
|
|
50
|
+
|
|
51
|
+
## API reference (`cloud_brain`)
|
|
52
|
+
|
|
53
|
+
- **`crowddrop_sdk.cloud_brain.events.DRONE_COMMANDS`** - the eight
|
|
54
|
+
supported movement primitives, each a fixed pulse with no
|
|
55
|
+
duration/distance parameter: `take_off`, `land`, `forward`, `backward`,
|
|
56
|
+
`strafe_left`, `strafe_right`, `turn_left`, `turn_right`.
|
|
57
|
+
- **`DroneCommandEvent(drone_id, command, issued_at, sequence)`** - what you
|
|
58
|
+
receive, one per command.
|
|
59
|
+
- **`DroneTelemetryEvent(drone_id, latitude, longitude, heading,
|
|
60
|
+
battery_level, reported_at, sequence)`** - what you publish back.
|
|
61
|
+
- **`crowddrop_sdk.cloud_brain.drone.channels.DroneCommandSubscriber`** -
|
|
62
|
+
pull-based subscriber for commands (you always initiate the connection
|
|
63
|
+
outward - nothing is ever pushed to your device).
|
|
64
|
+
- **`crowddrop_sdk.cloud_brain.drone.channels.DroneTelemetryPublisher`** -
|
|
65
|
+
publisher for telemetry, with a `publish_telemetry(drone_id, latitude,
|
|
66
|
+
longitude, heading, battery_level)` convenience method.
|
|
67
|
+
- **Two integration points you implement** - `handle_command(event)` (map
|
|
68
|
+
each command to your real flight-controller call) and
|
|
69
|
+
`read_battery_and_gps()` (return a real sensor reading). Both are stand-ins
|
|
70
|
+
in the example below; this SDK doesn't know your hardware's API.
|
|
71
|
+
|
|
72
|
+
## Quickstart
|
|
73
|
+
|
|
74
|
+
See [`cloud_brain/drone/README.md`](crowddrop_sdk/cloud_brain/drone/README.md)
|
|
75
|
+
for a runnable example (`edge_agent.py`) and the exact env vars it needs.
|
|
76
|
+
|
|
77
|
+
## Releasing (publishing a new version to PyPI)
|
|
78
|
+
|
|
79
|
+
Releases are tag-triggered via `.github/workflows/publish-crowddrop-sdk.yml`,
|
|
80
|
+
using PyPI's **Trusted Publishing** (OIDC) — no API token is stored as a
|
|
81
|
+
GitHub secret.
|
|
82
|
+
|
|
83
|
+
1. Bump `version` in `crowddrop_sdk/pyproject.toml`.
|
|
84
|
+
2. Commit that change (on a branch, via the normal PR flow).
|
|
85
|
+
3. Once merged, tag the merge commit and push the tag:
|
|
86
|
+
```bash
|
|
87
|
+
git tag crowddrop-sdk-v<version> # e.g. crowddrop-sdk-v0.1.1
|
|
88
|
+
git push origin crowddrop-sdk-v<version>
|
|
89
|
+
```
|
|
90
|
+
The tag push is what fires the workflow — it builds `crowddrop_sdk/` and
|
|
91
|
+
uploads it to [pypi.org/project/crowddrop-sdk](https://pypi.org/project/crowddrop-sdk/).
|
|
92
|
+
No other trigger publishes this package.
|
|
93
|
+
|
|
94
|
+
If `cloud-brain`'s dependency on `crowddrop-pubsub-sdk` (see `pyproject.toml`)
|
|
95
|
+
needs bumping too, release `pubsub_sdk` first — see its own README's
|
|
96
|
+
"Releasing" section, same mechanism, separate workflow/tag prefix
|
|
97
|
+
(`pubsub-sdk-v*`).
|
|
98
|
+
|
|
99
|
+
**One-time setup, not yet done as of this writing — needed before the first
|
|
100
|
+
tag push, and again only if this ever moves to a different PyPI
|
|
101
|
+
account/org:**
|
|
102
|
+
- Register a **pending publisher** for `crowddrop-sdk` at
|
|
103
|
+
https://pypi.org/manage/account/publishing/ — this can be done before the
|
|
104
|
+
PyPI project exists, so it covers the *first-ever* release too, not just
|
|
105
|
+
subsequent ones. Fill in: PyPI project name `crowddrop-sdk`, repo owner
|
|
106
|
+
`crowddrop-ai`, repo name `crowddrop_ai_agents`, workflow filename
|
|
107
|
+
`publish-crowddrop-sdk.yml`, environment name `pypi`. Requires a PyPI
|
|
108
|
+
account with 2FA enabled — no API token to generate or store.
|
|
109
|
+
- The `pypi` GitHub Environment referenced by the workflow is created
|
|
110
|
+
automatically the first time the workflow runs against it; create it
|
|
111
|
+
manually in this repo's Settings → Environments beforehand only if you
|
|
112
|
+
want a required-reviewer protection rule (so a tag push pauses for human
|
|
113
|
+
approval before it actually publishes).
|
|
114
|
+
- `../scripts/publish_python_packages.sh` (manual `build` + `twine upload`)
|
|
115
|
+
is kept as a fallback/local-dry-run tool only — with a pending publisher
|
|
116
|
+
registered, it's no longer needed even for the first release.
|
|
117
|
+
|
|
118
|
+
Versioning is manual — nothing cross-checks the tag against
|
|
119
|
+
`pyproject.toml`'s `version`. Bump the file first, commit, *then* tag that
|
|
120
|
+
exact commit; tagging a commit whose `pyproject.toml` still has an
|
|
121
|
+
already-published version will fail the upload (PyPI rejects re-uploading an
|
|
122
|
+
existing version).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
crowddrop_sdk/__init__.py
|
|
4
|
+
crowddrop_sdk.egg-info/PKG-INFO
|
|
5
|
+
crowddrop_sdk.egg-info/SOURCES.txt
|
|
6
|
+
crowddrop_sdk.egg-info/dependency_links.txt
|
|
7
|
+
crowddrop_sdk.egg-info/requires.txt
|
|
8
|
+
crowddrop_sdk.egg-info/top_level.txt
|
|
9
|
+
crowddrop_sdk/cloud_brain/__init__.py
|
|
10
|
+
crowddrop_sdk/cloud_brain/events.py
|
|
11
|
+
crowddrop_sdk/cloud_brain/drone/__init__.py
|
|
12
|
+
crowddrop_sdk/cloud_brain/drone/channels.py
|
|
13
|
+
crowddrop_sdk/cloud_brain/drone/edge_agent.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
crowddrop_sdk
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "crowddrop-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "CrowdDrop's SDK for embodied-agent hardware. This release covers cloud_brain: the LLM stays in the cloud, the device is a thin actuator/sensor bridge over Pub/Sub."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
[project.optional-dependencies]
|
|
10
|
+
cloud-brain = [
|
|
11
|
+
"crowddrop-pubsub-sdk>=0.2.0,<1.0.0",
|
|
12
|
+
"requests>=2.28.0,<3.0.0",
|
|
13
|
+
"google-auth>=2.29.0,<3.0.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Repository = "https://github.com/crowddrop-ai/crowddrop_ai_agents"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["setuptools"]
|
|
21
|
+
build-backend = "setuptools.build_meta"
|