fct-kiwi


Namefct-kiwi JSON
Version 0.0.4 PyPI version JSON
download
home_pagehttps://github.com/gokiwibot/fct-package
SummaryFCT balena API
upload_time2024-11-23 17:19:06
maintainerNone
docs_urlNone
authorFrancisco Valbuena
requires_python<4.0,>=3.10
licenseMIT
keywords balena fleet control
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # fct-kiwi

## Brief

The fct-kiwi package offers a object oriented API to the balena API, it helps the developer to effortless optimize API request load and the program's memory by managing a program-global data structure which manages each resource requested to the Balena Cloud.

## Basics

### Authentication

Before starting using the libraries, you will need to authenticate into your balena account using an API key:

#### Option 1 environment variable

Add your API key to the env named `BALENA_API_KEY`

#### Option 2 using the authenticate class method

```python3
from fct_kiwi import Balena

Balena.authenticate(balena_api_key)
```

you can always check if your API key is loaded using the `Balena.id_authenticated()` method

```python3
if not Balena.is_authenticated():
    print("Your Balena API key is not loaded yet", file=stderr)
```

### Resources

[Balena resources](https://docs.balena.io/reference/api/resources/device/) are mapped into classes, which are children of the `Resource` base class; we will use their respective methods to manage the resources.

This approach is much simpler and powerful than using the web API directly, for instance, let's say you want to list the devices belonging to a fleet's id.
By using the web API, you would have to handle the following request:

```bash
curl -X GET \
"https://api.balena-cloud.com/v6/device?\$filter=belongs_to__application%20eq%20'<FLEET ID>'" \
-H  "Content-Type: application/json"  \
-H "Authorization: Bearer <AUTH_TOKEN>"
```

After proper configuration, this would be the equivalent using the package:
```python3
devices = Device.fromFleet(fleet_id=000)
```

in this example, `Device` is a class with inherited properties from `Resource`.

Resource classes are ORM like objects, that's because balena resources are tables of arelational database, thus, when we fetch a resource by using class methods like `Device.fromFleet` or updating existing ones with methods like `devices.update()`, the data returned from the web API is updated into a data structure in the class scope, which you can check by using `Resource.get_registry()`.

```python3
from fct_kiwi import Device

print(Device.get_registry()) # Check the registry before fetching a new resource

device = Device(8026455)     # Get a device using it's id

print(Device.get_registry()) # The device is not resistered if the object is created using it's consturctor

Device.register(device)      # Register the device manually
print(Device.get_registry()) # The device is not resistered if the object is created using it's consturctor and it's id (which is rare to know beforehand)

device.update()              # fetch the device resource on balena cloud
device2 = Device.fromDeviceName(device['device_name']) # Trying to fetch the same device
                            # fromDeviceName runs Device.register internallt

print(Device.get_registry()) # The registry still has only one device
                             # The device name is prefered when printing a resource group instead of the device id if available 
                             # Now device and device2 are references to the same Device type object
```

if you run the code above, expect a response like the following:

```bash
None
None
<ResourceGroup[Device]>: [0000000] 
<ResourceGroup[Device]>: [kiwibot4U063]
```

### Resource not found

Sometimes we will request a resource that doesn't exist in balena Cloud, in this cases, any try to fetch this resource will raise an `ResourceNotFound` exception, which you can import from the package.

```python3
from fct_kiwi import ResourceNotFound
```

You can create personalized resource groups in the following manner:

Example: ResourceGroup that contains all fleet service variables for a given service id
```python3
from fct_kiwi import FleetServiceEnv, ResourceGroup

serv_envs = ResourceGroup(FleetServiceEnv)
serv_envs.filter=f"service eq {1....0}"
serv_envs.update()

print(serv_envs)
```

### Accessing resource information

Each resource object is a row of a table on a database; it works like a dictionary on python, so you can access the resource fields just like if it was a dictionary:

```python3
print(device['device_name'])
print(device.keys())
print(device.values())
```
These methods are a shortcut to access to the real dictionary saved in the object, if you need to access it directly, it is saved in:
```python3
device.data: dict
```
Every Resource has diferent fields, when working with them, have in hand the [official resources documentation](https://docs.balena.io/reference/api/resources/device/)

### Foreign resources

Each resource comes with one or more foreign keys refering to other resources, this means a sole call to `device.update()` will also fetch other resources related to the device:

For example, the `device` resource contains fields called `belongs_to__application` and  `is_running__release` which are foreign keys to other resources, when fetching the device default table, their information will also be updated in their respective data structure:

```python3
from fct_kiwi import Device, Resource

device = Device(0000000)     # Get a device using it's id
Device.register(device)      # Register the device manually
device.update()              # fetch the device resource on balena cloud

# Trick to print all registrers
for cls in Resource.__subclasses__():
    print(cls.get_registry())
```

output

```bash
Nonebeter
None
None
None
None
None
<ResourceGroup[Device]>: [kiwibot4U063] 
None
<ResourceGroup[Fleet]>: [1....2] 
<ResourceGroup[Release]>: [3....0] 
```

you can access foreign resources from the origiginal one, for instance, to access the fleet of a device, you could just call the `get_fleet()` method:

```python3
fleet = device.get_fleet()
```

### Resource Group

ResourceGroup is a class that contains an up to date data structure containing all devices that fulfill a common condition. It is better understandable with an example:

```python3
devices = Device.fromFleet(fleet_id=<fid>) # Returns a resource group
```

`Devices.fromFleet()` returns a resource group with all devices that belong to the fleet specifiedwith the parameter `fleet_id`, now you can use `devices.update()` each time you want to sync the information of the devices with Balena cloud.

#### updating resource groups

It is better to use the `update` method in a resource group than to loop through a list of devices calling the Resource scoped `update` method, since resource group fetches the entire resource group in a single request instead of looping through every resource.

If a resource contained in a resource group is deleted (for instance, using the `delete() method`), this resource will be removed from all existing resource groups. For this reason, if you are looping through a resource group while it is possible that a resource is deleted, then you would need to make a copy of the data structure and loop through the copy instead, here an example:

```python3
for device in list(devices.values()):
    # may call device.delete()
```

### Available resources

Currently, the package has mapped the following resources into classes:

* `Device`
* `DeviceEnv`
* `DeviceServiceEnv`
* `DeviceTag`
* `Fleet`
* `FleetEnv`
* `FleetServiceEnv`
* `Release`
* `Service`
* `ServiceInstall`

These are not near all resources Balena offers, but these are the ones necessary to do some fleet and devices management, including release updates and envs.


## Envs

In Balena, there are 4 kind of environment variables:

1. Those which are attached to a device: `DeviceEnv`
2. Those which are attached to a service installed in a device: `DeviceServiceEnv`
3. Those which are attached to a fleet: `FleetEnv`
4. Those which are attached to a service belonging to a fleet: `FleetServiceEnv`

Devices belonging to a fleet load all the fleet variables by default and then overwrites those with matching names in the device variables.

### Deleting envs

fetch the variables you want to delete and call the `delete()` method, here is an one-liner:
```python3
DeviceEnv.fromDeviceAndName(<DEVICE ID>, <VARIABLE NAME>).delete()
```
note: you can use `delete()` with resource groups to delete several resources at once.

### Changing and creating device variables

If the variable already exists:

```python3
success = device.overwrite_env(var_name, new_value)
```
Note: `new_value` is always of type string, it will be later parsed to int or bool by Balena cloud during the request performed.

`overwrite_env` all the service and environment variables of both the device and fleet where the device belongs; envs matching `var_name` and overwrites it's value, if there is not a variable matching `var_name`, then the call fails and the method returns `False`

If the variable doesn't exist, or it is repeated between two or more services, this call won't be of much help. If you know the name of the service where the variable belongs, whe highly recommend you to use `set_env()`:

```python3
success = set_env(var_name, new_value, service_name="*")
```

The variable of the specified service name is changed, if it is a `DeviceEnv` rather than a `DeviceServiceEnv`, pass `None` or `"*"` as the parameter. If the variable with those specifications doesn't exist, it will create it.

Returns False on error.

### Changing fleet variables

fleets support `set_env()` method:

```python3
success = set_env(var_name, new_value, service_name="*")
```

Having variables gathered in 4 different classes can make scripts get large, so we tried to keep it simple by including the `set_env` method, here is an implementation of `set_fleet_env` you can try:

```python3
from fct_kiwi import ResourceNotFound, Fleet, FleetEnv, FleetServiceEnv, Service

def set_fleet_env(fleet_name: str, name: str, value: str, service_name: str | None = "*") -> bool:

    # Getting the fleet object
    try:
        fleet: Fleet = Fleet.fromAppName(fleet_name, select="id");
    except ResourceNotFound:
        print (f"fleet of name {fleet_name} doesn't exist")
        return False
    if fleet.id is None:
        return False  # Unexpected error
    
    # Handle FleetEnv
    if service_name is None or service_name == "*":
        try:
            env = FleetEnv.fromFleetAndName(fleet.id, name)
            env.patch({"value": value})
        except ResourceNotFound:
            # The variable doesn't exist, then create it
            FleetEnv.new({"application": fleet.id, "name": name, "value": value})

    # Handle FleetServiceEnv
    else:
        # Getting Service
        try:
            service = Service.fromFleetAndName(
                    fleet_id=fleet.id,
                    service_name=service_name)
            if service.id == None:
                return False # Unexpected error
        except ResourceNotFound:
            print (f"service of name {service_name} doesn't exist")
            return False

        try:
            env = FleetServiceEnv.fromServiceAndName(service_id=service.id, name=name)
            env.parch({"value": value})
        except ResourceNotFound:
            # The variable doesn't exist, then create it
            FleetEnv.new({"service": service.id, "name": name, "value": value})

    return True
```

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/gokiwibot/fct-package",
    "name": "fct-kiwi",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.10",
    "maintainer_email": null,
    "keywords": "balena, fleet control",
    "author": "Francisco Valbuena",
    "author_email": "f.valbuenao64@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/c8/76/d670ea25cf0a92cbb0198a25428f0e188bd9e703efb7532ecb4bb58cd11d/fct_kiwi-0.0.4.tar.gz",
    "platform": null,
    "description": "# fct-kiwi\n\n## Brief\n\nThe fct-kiwi package offers a object oriented API to the balena API, it helps the developer to effortless optimize API request load and the program's memory by managing a program-global data structure which manages each resource requested to the Balena Cloud.\n\n## Basics\n\n### Authentication\n\nBefore starting using the libraries, you will need to authenticate into your balena account using an API key:\n\n#### Option 1 environment variable\n\nAdd your API key to the env named `BALENA_API_KEY`\n\n#### Option 2 using the authenticate class method\n\n```python3\nfrom fct_kiwi import Balena\n\nBalena.authenticate(balena_api_key)\n```\n\nyou can always check if your API key is loaded using the `Balena.id_authenticated()` method\n\n```python3\nif not Balena.is_authenticated():\n    print(\"Your Balena API key is not loaded yet\", file=stderr)\n```\n\n### Resources\n\n[Balena resources](https://docs.balena.io/reference/api/resources/device/) are mapped into classes, which are children of the `Resource` base class; we will use their respective methods to manage the resources.\n\nThis approach is much simpler and powerful than using the web API directly, for instance, let's say you want to list the devices belonging to a fleet's id.\nBy using the web API, you would have to handle the following request:\n\n```bash\ncurl -X GET \\\n\"https://api.balena-cloud.com/v6/device?\\$filter=belongs_to__application%20eq%20'<FLEET ID>'\" \\\n-H  \"Content-Type: application/json\"  \\\n-H \"Authorization: Bearer <AUTH_TOKEN>\"\n```\n\nAfter proper configuration, this would be the equivalent using the package:\n```python3\ndevices = Device.fromFleet(fleet_id=000)\n```\n\nin this example, `Device` is a class with inherited properties from `Resource`.\n\nResource classes are ORM like objects, that's because balena resources are tables of arelational database, thus, when we fetch a resource by using class methods like `Device.fromFleet` or updating existing ones with methods like `devices.update()`, the data returned from the web API is updated into a data structure in the class scope, which you can check by using `Resource.get_registry()`.\n\n```python3\nfrom fct_kiwi import Device\n\nprint(Device.get_registry()) # Check the registry before fetching a new resource\n\ndevice = Device(8026455)     # Get a device using it's id\n\nprint(Device.get_registry()) # The device is not resistered if the object is created using it's consturctor\n\nDevice.register(device)      # Register the device manually\nprint(Device.get_registry()) # The device is not resistered if the object is created using it's consturctor and it's id (which is rare to know beforehand)\n\ndevice.update()              # fetch the device resource on balena cloud\ndevice2 = Device.fromDeviceName(device['device_name']) # Trying to fetch the same device\n                            # fromDeviceName runs Device.register internallt\n\nprint(Device.get_registry()) # The registry still has only one device\n                             # The device name is prefered when printing a resource group instead of the device id if available \n                             # Now device and device2 are references to the same Device type object\n```\n\nif you run the code above, expect a response like the following:\n\n```bash\nNone\nNone\n<ResourceGroup[Device]>: [0000000] \n<ResourceGroup[Device]>: [kiwibot4U063]\n```\n\n### Resource not found\n\nSometimes we will request a resource that doesn't exist in balena Cloud, in this cases, any try to fetch this resource will raise an `ResourceNotFound` exception, which you can import from the package.\n\n```python3\nfrom fct_kiwi import ResourceNotFound\n```\n\nYou can create personalized resource groups in the following manner:\n\nExample: ResourceGroup that contains all fleet service variables for a given service id\n```python3\nfrom fct_kiwi import FleetServiceEnv, ResourceGroup\n\nserv_envs = ResourceGroup(FleetServiceEnv)\nserv_envs.filter=f\"service eq {1....0}\"\nserv_envs.update()\n\nprint(serv_envs)\n```\n\n### Accessing resource information\n\nEach resource object is a row of a table on a database; it works like a dictionary on python, so you can access the resource fields just like if it was a dictionary:\n\n```python3\nprint(device['device_name'])\nprint(device.keys())\nprint(device.values())\n```\nThese methods are a shortcut to access to the real dictionary saved in the object, if you need to access it directly, it is saved in:\n```python3\ndevice.data: dict\n```\nEvery Resource has diferent fields, when working with them, have in hand the [official resources documentation](https://docs.balena.io/reference/api/resources/device/)\n\n### Foreign resources\n\nEach resource comes with one or more foreign keys refering to other resources, this means a sole call to `device.update()` will also fetch other resources related to the device:\n\nFor example, the `device` resource contains fields called `belongs_to__application` and  `is_running__release` which are foreign keys to other resources, when fetching the device default table, their information will also be updated in their respective data structure:\n\n```python3\nfrom fct_kiwi import Device, Resource\n\ndevice = Device(0000000)     # Get a device using it's id\nDevice.register(device)      # Register the device manually\ndevice.update()              # fetch the device resource on balena cloud\n\n# Trick to print all registrers\nfor cls in Resource.__subclasses__():\n    print(cls.get_registry())\n```\n\noutput\n\n```bash\nNonebeter\nNone\nNone\nNone\nNone\nNone\n<ResourceGroup[Device]>: [kiwibot4U063] \nNone\n<ResourceGroup[Fleet]>: [1....2] \n<ResourceGroup[Release]>: [3....0] \n```\n\nyou can access foreign resources from the origiginal one, for instance, to access the fleet of a device, you could just call the `get_fleet()` method:\n\n```python3\nfleet = device.get_fleet()\n```\n\n### Resource Group\n\nResourceGroup is a class that contains an up to date data structure containing all devices that fulfill a common condition. It is better understandable with an example:\n\n```python3\ndevices = Device.fromFleet(fleet_id=<fid>) # Returns a resource group\n```\n\n`Devices.fromFleet()` returns a resource group with all devices that belong to the fleet specifiedwith the parameter `fleet_id`, now you can use `devices.update()` each time you want to sync the information of the devices with Balena cloud.\n\n#### updating resource groups\n\nIt is better to use the `update` method in a resource group than to loop through a list of devices calling the Resource scoped `update` method, since resource group fetches the entire resource group in a single request instead of looping through every resource.\n\nIf a resource contained in a resource group is deleted (for instance, using the `delete() method`), this resource will be removed from all existing resource groups. For this reason, if you are looping through a resource group while it is possible that a resource is deleted, then you would need to make a copy of the data structure and loop through the copy instead, here an example:\n\n```python3\nfor device in list(devices.values()):\n    # may call device.delete()\n```\n\n### Available resources\n\nCurrently, the package has mapped the following resources into classes:\n\n* `Device`\n* `DeviceEnv`\n* `DeviceServiceEnv`\n* `DeviceTag`\n* `Fleet`\n* `FleetEnv`\n* `FleetServiceEnv`\n* `Release`\n* `Service`\n* `ServiceInstall`\n\nThese are not near all resources Balena offers, but these are the ones necessary to do some fleet and devices management, including release updates and envs.\n\n\n## Envs\n\nIn Balena, there are 4 kind of environment variables:\n\n1. Those which are attached to a device: `DeviceEnv`\n2. Those which are attached to a service installed in a device: `DeviceServiceEnv`\n3. Those which are attached to a fleet: `FleetEnv`\n4. Those which are attached to a service belonging to a fleet: `FleetServiceEnv`\n\nDevices belonging to a fleet load all the fleet variables by default and then overwrites those with matching names in the device variables.\n\n### Deleting envs\n\nfetch the variables you want to delete and call the `delete()` method, here is an one-liner:\n```python3\nDeviceEnv.fromDeviceAndName(<DEVICE ID>, <VARIABLE NAME>).delete()\n```\nnote: you can use `delete()` with resource groups to delete several resources at once.\n\n### Changing and creating device variables\n\nIf the variable already exists:\n\n```python3\nsuccess = device.overwrite_env(var_name, new_value)\n```\nNote: `new_value` is always of type string, it will be later parsed to int or bool by Balena cloud during the request performed.\n\n`overwrite_env` all the service and environment variables of both the device and fleet where the device belongs; envs matching `var_name` and overwrites it's value, if there is not a variable matching `var_name`, then the call fails and the method returns `False`\n\nIf the variable doesn't exist, or it is repeated between two or more services, this call won't be of much help. If you know the name of the service where the variable belongs, whe highly recommend you to use `set_env()`:\n\n```python3\nsuccess = set_env(var_name, new_value, service_name=\"*\")\n```\n\nThe variable of the specified service name is changed, if it is a `DeviceEnv` rather than a `DeviceServiceEnv`, pass `None` or `\"*\"` as the parameter. If the variable with those specifications doesn't exist, it will create it.\n\nReturns False on error.\n\n### Changing fleet variables\n\nfleets support `set_env()` method:\n\n```python3\nsuccess = set_env(var_name, new_value, service_name=\"*\")\n```\n\nHaving variables gathered in 4 different classes can make scripts get large, so we tried to keep it simple by including the `set_env` method, here is an implementation of `set_fleet_env` you can try:\n\n```python3\nfrom fct_kiwi import ResourceNotFound, Fleet, FleetEnv, FleetServiceEnv, Service\n\ndef set_fleet_env(fleet_name: str, name: str, value: str, service_name: str | None = \"*\") -> bool:\n\n    # Getting the fleet object\n    try:\n        fleet: Fleet = Fleet.fromAppName(fleet_name, select=\"id\");\n    except ResourceNotFound:\n        print (f\"fleet of name {fleet_name} doesn't exist\")\n        return False\n    if fleet.id is None:\n        return False  # Unexpected error\n    \n    # Handle FleetEnv\n    if service_name is None or service_name == \"*\":\n        try:\n            env = FleetEnv.fromFleetAndName(fleet.id, name)\n            env.patch({\"value\": value})\n        except ResourceNotFound:\n            # The variable doesn't exist, then create it\n            FleetEnv.new({\"application\": fleet.id, \"name\": name, \"value\": value})\n\n    # Handle FleetServiceEnv\n    else:\n        # Getting Service\n        try:\n            service = Service.fromFleetAndName(\n                    fleet_id=fleet.id,\n                    service_name=service_name)\n            if service.id == None:\n                return False # Unexpected error\n        except ResourceNotFound:\n            print (f\"service of name {service_name} doesn't exist\")\n            return False\n\n        try:\n            env = FleetServiceEnv.fromServiceAndName(service_id=service.id, name=name)\n            env.parch({\"value\": value})\n        except ResourceNotFound:\n            # The variable doesn't exist, then create it\n            FleetEnv.new({\"service\": service.id, \"name\": name, \"value\": value})\n\n    return True\n```\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "FCT balena API",
    "version": "0.0.4",
    "project_urls": {
        "Homepage": "https://github.com/gokiwibot/fct-package",
        "Repository": "https://github.com/gokiwibot/fct-package"
    },
    "split_keywords": [
        "balena",
        " fleet control"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "140062ca831900362283ee7535671abd89d5f0c53d665b82f416842d08803d61",
                "md5": "b76c1f59331e5c9f45857fb2157e8f14",
                "sha256": "f97636fc02075ff0dad73abd4c9d50634645e34917f0a3ebb81750e094a94d57"
            },
            "downloads": -1,
            "filename": "fct_kiwi-0.0.4-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "b76c1f59331e5c9f45857fb2157e8f14",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.10",
            "size": 21590,
            "upload_time": "2024-11-23T17:19:04",
            "upload_time_iso_8601": "2024-11-23T17:19:04.222533Z",
            "url": "https://files.pythonhosted.org/packages/14/00/62ca831900362283ee7535671abd89d5f0c53d665b82f416842d08803d61/fct_kiwi-0.0.4-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "c876d670ea25cf0a92cbb0198a25428f0e188bd9e703efb7532ecb4bb58cd11d",
                "md5": "085830793ff7d99259d9676049162a05",
                "sha256": "a1bbe930521849d85a80ee78b8add057d6dff6476276ad7a078a4614bd28589a"
            },
            "downloads": -1,
            "filename": "fct_kiwi-0.0.4.tar.gz",
            "has_sig": false,
            "md5_digest": "085830793ff7d99259d9676049162a05",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.10",
            "size": 16978,
            "upload_time": "2024-11-23T17:19:06",
            "upload_time_iso_8601": "2024-11-23T17:19:06.094963Z",
            "url": "https://files.pythonhosted.org/packages/c8/76/d670ea25cf0a92cbb0198a25428f0e188bd9e703efb7532ecb4bb58cd11d/fct_kiwi-0.0.4.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-11-23 17:19:06",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "gokiwibot",
    "github_project": "fct-package",
    "github_not_found": true,
    "lcname": "fct-kiwi"
}
        
Elapsed time: 1.22734s