diff --git a/docs/source/releases/changelog.rst b/docs/source/releases/changelog.rst index 0cd3ab666..f46188dac 100644 --- a/docs/source/releases/changelog.rst +++ b/docs/source/releases/changelog.rst @@ -25,6 +25,16 @@ brought up to date. accepts legacy format-1 backups (created by Kegbot v1.1.x) and upgrades their data in one step; no intermediate 1.2/1.3 install is needed. * Time zone choices are now derived from the system time zone database. +* **A new REST API is served at ``/api/``** (previously the experimental + ``/api/v2/``, now always enabled). An OpenAPI schema is available at + ``/api/schema``, with interactive documentation at ``/api/docs``. +* **The legacy HTTP API is deprecated and now lives only at ``/api/v1/``.** + It serves only the endpoints used by kegbot-pycore (plus the events + feed); every other endpoint returns ``410 Gone``, and all legacy + responses carry a ``Deprecation`` header. Protocol Buffers are no longer + used anywhere in the server. **kegbot-pycore users:** set the api url to + ``http:///api/v1/`` (the bare ``/api/`` prefix no longer serves + the legacy endpoints). **Upgrade notes** @@ -40,6 +50,77 @@ brought up to date. arm64 only). * The legacy gflags-based Python API client was removed from the server package; it lives in the separate kegbot-api project. +* **Breaking change: the following legacy API endpoints have been + removed** and now return ``410 Gone``: + + * ``GET /api/v1/version`` + * ``POST /api/v1/login`` + * ``GET|POST /api/v1/logout`` + * ``GET /api/v1/get-api-key`` + * ``POST /api/v1/devices/link`` + * ``GET /api/v1/devices/link/status`` + * ``GET /api/v1/devices/link/status/:code`` + * ``POST /api/v1/auth-tokens/:auth_device/:token_value/assign`` + * ``GET|POST|DELETE /api/v1/controllers/:id`` + * ``GET /api/v1/drinks`` + * ``GET /api/v1/drinks/last`` + * ``GET /api/v1/drinks/:id`` + * ``POST /api/v1/drinks/:id/add-photo`` + * ``GET|POST|DELETE /api/v1/flow-meters/:id`` + * ``GET|POST /api/v1/flow-toggles`` + * ``GET|POST|DELETE /api/v1/flow-toggles/:id`` + * ``GET /api/v1/kegs`` + * ``GET /api/v1/kegs/:id`` + * ``POST /api/v1/kegs/:id/end`` + * ``GET /api/v1/kegs/:id/drinks`` + * ``GET /api/v1/kegs/:id/events`` + * ``GET /api/v1/kegs/:id/sessions`` + * ``GET /api/v1/kegs/:id/stats`` + * ``GET /api/v1/keg-sizes`` + * ``POST /api/v1/pictures`` + * ``GET /api/v1/sessions`` + * ``GET /api/v1/sessions/current`` + * ``GET /api/v1/sessions/:id`` + * ``GET /api/v1/sessions/:id/stats`` + * ``DELETE /api/v1/taps/:meter_name_or_id`` + * ``POST /api/v1/taps/:meter_name_or_id/activate`` + * ``POST /api/v1/taps/:meter_name_or_id/calibrate`` + * ``POST /api/v1/taps/:meter_name_or_id/spill`` + * ``POST /api/v1/taps/:meter_name_or_id/connect-meter`` + * ``POST /api/v1/taps/:meter_name_or_id/disconnect-meter`` + * ``POST /api/v1/taps/:meter_name_or_id/connect-toggle`` + * ``POST /api/v1/taps/:meter_name_or_id/disconnect-toggle`` + * ``POST /api/v1/taps/:meter_name_or_id/connect-thermo`` + * ``POST /api/v1/taps/:meter_name_or_id/disconnect-thermo`` + * ``GET /api/v1/thermo-sensors`` + * ``GET /api/v1/thermo-sensors/:sensor_name/logs`` + * ``GET /api/v1/users`` + * ``GET /api/v1/users/:username`` + * ``GET /api/v1/users/:username/drinks`` + * ``GET /api/v1/users/:username/events`` + * ``GET /api/v1/users/:username/stats`` + * ``GET|POST /api/v1/users/:username/photo`` + * ``POST /api/v1/new-user`` + * ``GET /api/v1/stats`` + * ``GET /api/v1/sound-events`` + +* The following legacy API endpoints are deprecated, but remain supported + for compatibility with kegbot-pycore. They will be removed in a future + release: + + * ``GET /api/v1/status`` + * ``GET /api/v1/auth-tokens/:auth_device/:token_value`` + * ``POST /api/v1/cancel-drink`` + * ``GET|POST /api/v1/controllers`` + * ``GET /api/v1/events`` + * ``GET|POST /api/v1/flow-meters`` + * ``GET|POST /api/v1/taps`` + * ``GET|POST /api/v1/taps/:meter_name_or_id`` + * ``GET|POST /api/v1/thermo-sensors/:sensor_name`` + +* The old Kegbot mobile apps depended on now-retired API endpoints (device + linking, registration, drink lists) and no longer work against this + server. Version 1.3.0 (2022-08-10) diff --git a/docs/source/settings.rst b/docs/source/settings.rst index e1cd4cc6a..58cd49174 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -82,10 +82,3 @@ change and should only be needed by advanced users. be prefixed by this URL. Otherwise, media will be served from the same host as the server itself, under ``/media``. You may use this setting to e.g. serve media links through a CDN. - -.. data:: KEGBOT_ENABLE_V2_API - - If set to ``true``, the new Kegbot Server API will be enabled (at - path ``/api/v2/...``). This API will replace the existing API implementation - in a future major Kegbot release. It is currently unfinished. Enable this - if you are a developer intending to work on or with this API. diff --git a/pykeg/api/apps.py b/pykeg/api/apps.py index 878e7d54f..d6eca7bf0 100644 --- a/pykeg/api/apps.py +++ b/pykeg/api/apps.py @@ -3,4 +3,5 @@ class ApiConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "api" + name = "pykeg.api" + label = "pykeg_api" diff --git a/pykeg/api/auth.py b/pykeg/api/auth.py index bbbe9cf33..0ccdda6d6 100644 --- a/pykeg/api/auth.py +++ b/pykeg/api/auth.py @@ -1,3 +1,4 @@ +from drf_spectacular.extensions import OpenApiAuthenticationExtension from rest_framework import authentication from rest_framework.exceptions import AuthenticationFailed @@ -40,3 +41,17 @@ def authenticate_credentials(self, userid, password, request=None): 'For Basic Auth, provide "api" as the username and an API key as the password.' ) return validate_api_key(password) + + +class ApiKeyBasicAuthScheme(OpenApiAuthenticationExtension): + """Describes ApiKeyBasicAuth in the OpenAPI schema.""" + + target_class = "pykeg.api.auth.ApiKeyBasicAuth" + name = "apiKeyBasicAuth" + + def get_security_definition(self, auto_schema): + return { + "type": "http", + "scheme": "basic", + "description": 'Use "api" as the username and an API key as the password.', + } diff --git a/pykeg/api/models.py b/pykeg/api/models.py deleted file mode 100644 index 6b2021999..000000000 --- a/pykeg/api/models.py +++ /dev/null @@ -1 +0,0 @@ -# Create your models here. diff --git a/pykeg/api/permissions.py b/pykeg/api/permissions.py index 37b6ef6f9..56a9f6037 100644 --- a/pykeg/api/permissions.py +++ b/pykeg/api/permissions.py @@ -32,11 +32,7 @@ def has_permission(self, request, view): class IsAuthenticated(DashboardViewer): - """Permission for API resources which respects site-privacy. - - Resources are readable without authentication, *unless* the site's - `.privacy` setting is non-public. - """ + """Requires an authenticated user who also passes the site-privacy check.""" message = "You must log in to do that" @@ -44,3 +40,14 @@ def has_permission(self, request, view): if not super().has_permission(request, view): return False return bool(request.user and request.user.is_authenticated) + + +class AdminWriteDashboardRead(DashboardViewer): + """Reads follow site-privacy; writes require a staff user.""" + + message = "You must be an admin to do that" + + def has_permission(self, request, view): + if request.method in permissions.SAFE_METHODS: + return super().has_permission(request, view) + return bool(request.user and request.user.is_staff) diff --git a/pykeg/api/serializers.py b/pykeg/api/serializers.py index 86b9c40aa..a39bbdf15 100644 --- a/pykeg/api/serializers.py +++ b/pykeg/api/serializers.py @@ -1,3 +1,6 @@ +from django.contrib.auth import authenticate +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from rest_framework.exceptions import ValidationError @@ -25,15 +28,19 @@ class Meta: thumbnail_png_url = serializers.SerializerMethodField() caption = serializers.CharField() + @extend_schema_field(OpenApiTypes.URI) def get_resized_url(self, picture): return picture.resized.url if picture else None + @extend_schema_field(OpenApiTypes.URI) def get_resized_png_url(self, picture): return picture.resized_png.url if picture else None + @extend_schema_field(OpenApiTypes.URI) def get_thumbnail_url(self, picture): return picture.thumbnail.url if picture else None + @extend_schema_field(OpenApiTypes.URI) def get_thumbnail_png_url(self, picture): return picture.thumbnail_png.url if picture else None @@ -59,8 +66,8 @@ class Meta: "stats", ] - background_image = PictureSerializer() - stats = serializers.JSONField(source="get_stats") + background_image = PictureSerializer(read_only=True) + stats = serializers.JSONField(source="get_stats", read_only=True) class UserSerializer(serializers.ModelSerializer): @@ -79,7 +86,7 @@ class Meta: "is_active", ] - picture = PictureSerializer(source="mugshot") + picture = PictureSerializer(source="mugshot", read_only=True) class InvitationSerializer(serializers.ModelSerializer): @@ -93,6 +100,8 @@ class Meta: "is_expired", ] + is_expired = serializers.BooleanField(read_only=True) + class DeviceSerializer(serializers.ModelSerializer): class Meta: @@ -118,6 +127,7 @@ class Meta: ] is_active = serializers.BooleanField(source="active") + key = serializers.CharField(read_only=True) class BeverageProducerSerializer(serializers.ModelSerializer): @@ -143,6 +153,7 @@ class Meta: "id", "name", "producer", + "producer_id", "beverage_type", "style", "description", @@ -160,7 +171,10 @@ class Meta: "untappd_beer_id", ] - producer = BeverageProducerSerializer() + producer = BeverageProducerSerializer(read_only=True) + producer_id = serializers.PrimaryKeyRelatedField( + queryset=models.BeverageProducer.objects.all(), source="producer", write_only=True + ) class ControllerSerializer(serializers.ModelSerializer): @@ -230,10 +244,10 @@ class Meta: "stats", ] - beverage = BeverageSerializer(source="type") - illustration = serializers.URLField(source="get_illustration") - illustration_thumbnail = serializers.URLField(source="get_illustration_thumb") - stats = serializers.JSONField(source="get_stats") + beverage = BeverageSerializer(source="type", read_only=True) + illustration = serializers.URLField(source="get_illustration", read_only=True) + illustration_thumbnail = serializers.URLField(source="get_illustration_thumb", read_only=True) + stats = serializers.JSONField(source="get_stats", read_only=True) class KegTapSerializer(serializers.ModelSerializer): @@ -249,7 +263,7 @@ class Meta: "current_keg", ] - current_keg = KegSerializer() + current_keg = KegSerializer(read_only=True) class DrinkSerializer(serializers.ModelSerializer): @@ -268,9 +282,9 @@ class Meta: "picture", ] - picture = PictureSerializer() - user = UserSerializer() - keg = KegSerializer() + picture = PictureSerializer(read_only=True) + user = UserSerializer(read_only=True) + keg = KegSerializer(read_only=True) class AuthenticationTokenSerializer(serializers.ModelSerializer): @@ -302,7 +316,7 @@ class Meta: "stats", ] - stats = serializers.JSONField(source="get_stats") + stats = serializers.JSONField(source="get_stats", read_only=True) class ThermoSensorSerializer(serializers.ModelSerializer): @@ -338,10 +352,10 @@ class Meta: "session", ] - drink = DrinkSerializer() - keg = KegSerializer() - user = UserSerializer() - session = DrinkingSessionSerializer() + drink = DrinkSerializer(read_only=True) + keg = KegSerializer(read_only=True) + user = UserSerializer(read_only=True) + session = DrinkingSessionSerializer(read_only=True) class NotificationSettingsSerializer(serializers.ModelSerializer): @@ -373,13 +387,8 @@ class LoginSerializer(serializers.Serializer): password = serializers.CharField() def validate(self, data): - username = data["username"] - password = data["password"] - try: - user = models.User.objects.get(username=username) - except models.User.DoesNotExist: - raise ValidationError("Incorrect username/password") - if not user.check_password(password): + user = authenticate(username=data["username"], password=data["password"]) + if not user: raise ValidationError("Incorrect username/password") data["user"] = user return data @@ -403,7 +412,7 @@ class Meta: "email", ] - picture = PictureSerializer(source="mugshot") + picture = PictureSerializer(source="mugshot", read_only=True) class SystemStatusSerializer(serializers.Serializer): diff --git a/pykeg/api/tests.py b/pykeg/api/tests.py index d2b4b1fba..2b15ac6a8 100644 --- a/pykeg/api/tests.py +++ b/pykeg/api/tests.py @@ -16,7 +16,6 @@ def add_auth(self): if self.api_key: credentials = f"api:{self.api_key}" base64_credentials = base64.b64encode(credentials.encode()).decode() - print("credentials", base64_credentials) self.client.credentials(HTTP_AUTHORIZATION=f"Basic {base64_credentials}") else: self.client.credentials() @@ -27,7 +26,7 @@ def get(self, *args, **kwargs): return response.status_code, response.json() def get_events(self): - return self.get("/api/v2/events") + return self.get("/api/events") class V2ApiTestCase(TestCase): @@ -117,3 +116,60 @@ def test_api_key_from_inactive_account_is_rejected(self): self.user.save() status, _ = self.client.get_events() self.assertEqual(403, status) + + +class V2ApiPermissionsTestCase(TestCase): + fixtures = ["testdata/demo-site.json"] + + def setUp(self): + self.client = ApiClient() + self.site = models.KegbotSite.objects.all().first() + self.site.server_version = get_version() + self.site.save() + self.member = models.User.objects.get(username="alice") + self.member_key = models.ApiKey.objects.get_or_create(user=self.member)[0] + + def test_users_are_read_only(self): + self.client.api_key = self.member_key.key + self.client.add_auth() + response = self.client.client.patch( + f"/api/users/{self.member.id}", {"display_name": "hax"}, format="json" + ) + self.assertEqual(405, response.status_code) + + def test_plugin_data_requires_admin(self): + self.client.api_key = self.member_key.key + status, _ = self.client.get("/api/plugin-data") + self.assertEqual(403, status) + + def test_beverages_require_admin_to_write(self): + self.client.api_key = self.member_key.key + self.client.add_auth() + response = self.client.client.post("/api/beverages", {"name": "Nope"}, format="json") + self.assertEqual(403, response.status_code) + + def test_notification_settings_are_scoped_to_caller(self): + other = models.User.objects.get(username="bob") + models.NotificationSettings.objects.create(user=other, backend="test", keg_tapped=True) + self.client.api_key = self.member_key.key + status, data = self.client.get("/api/notification-settings") + self.assertEqual(200, status) + self.assertEqual([], data["results"]) + + +class SchemaTestCase(TestCase): + fixtures = ["testdata/demo-site.json"] + + def setUp(self): + site = models.KegbotSite.objects.all().first() + site.server_version = get_version() + site.save() + + def test_schema(self): + response = self.client.get("/api/schema") + self.assertEqual(200, response.status_code) + self.assertIn("openapi", response.headers["Content-Type"]) + + def test_docs(self): + response = self.client.get("/api/docs") + self.assertEqual(200, response.status_code) diff --git a/pykeg/api/urls.py b/pykeg/api/urls.py index f75341684..f7e395d1f 100644 --- a/pykeg/api/urls.py +++ b/pykeg/api/urls.py @@ -1,4 +1,5 @@ from django.urls import include, path +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from rest_framework import routers from . import views @@ -32,4 +33,6 @@ path("auth/login", views.login), path("auth/logout", views.logout), path("status", views.system_status), + path("schema", SpectacularAPIView.as_view(), name="api-schema"), + path("docs", SpectacularSwaggerView.as_view(url_name="api-schema"), name="api-docs"), ] diff --git a/pykeg/api/views.py b/pykeg/api/views.py index 91c2f3998..e41dc8ff6 100644 --- a/pykeg/api/views.py +++ b/pykeg/api/views.py @@ -1,5 +1,7 @@ from django.contrib.auth import login as auth_login from django.contrib.auth import logout as auth_logout +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema from rest_framework import viewsets from rest_framework.decorators import ( api_view, @@ -13,10 +15,11 @@ from . import permissions, serializers -class UserViewSet(viewsets.ModelViewSet): +class UserViewSet(viewsets.ReadOnlyModelViewSet): """Lists all users in the system. - Public view for any authenticated caller. + Read-only view for any authenticated caller; user management happens + in the admin dashboard. """ queryset = models.User.objects.all() @@ -24,11 +27,11 @@ class UserViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated] -class InvitationViewSet(viewsets.ModelViewSet): +class InvitationViewSet(viewsets.ReadOnlyModelViewSet): """Lists all of the *current user's* invitations.""" queryset = models.Invitation.objects.all() - serializer_class = serializers.UserSerializer + serializer_class = serializers.InvitationSerializer permission_classes = [permissions.IsAuthenticated] def get_queryset(self): @@ -60,6 +63,9 @@ class ApiKeyViewSet(viewsets.ModelViewSet): serializer_class = serializers.ApiKeySerializer permission_classes = [permissions.IsAuthenticated] + def perform_create(self, serializer): + serializer.save(user=self.request.user) + def get_queryset(self): return ( super() @@ -76,7 +82,7 @@ class BeverageProducerViewSet(viewsets.ModelViewSet): queryset = models.BeverageProducer.objects.all() serializer_class = serializers.BeverageProducerSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [permissions.AdminWriteDashboardRead] class BeverageViewSet(viewsets.ModelViewSet): @@ -84,7 +90,7 @@ class BeverageViewSet(viewsets.ModelViewSet): queryset = models.Beverage.objects.all() serializer_class = serializers.BeverageSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [permissions.AdminWriteDashboardRead] class KegTapViewSet(viewsets.ReadOnlyModelViewSet): @@ -159,16 +165,16 @@ class ThermoSensorViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAdminUser] -class ThermologViewSet(viewsets.ModelViewSet): +class ThermologViewSet(viewsets.ReadOnlyModelViewSet): """Lists all Thermologs in the system.""" queryset = models.Thermolog.objects.all() serializer_class = serializers.ThermologSerializer - permission_classes = [permissions.IsAdminUser] + permission_classes = [permissions.DashboardViewer] class StatsViewSet(viewsets.ReadOnlyModelViewSet): - """Lists all Statss in the system.""" + """Lists all stats snapshots in the system.""" queryset = models.Stats.objects.all() serializer_class = serializers.StatsSerializer @@ -184,21 +190,31 @@ class SystemEventViewSet(viewsets.ReadOnlyModelViewSet): class NotificationSettingsViewSet(viewsets.ModelViewSet): - """Lists all NotificationSettingss in the system.""" + """Lists the *current user's* notification settings.""" queryset = models.NotificationSettings.objects.all() serializer_class = serializers.NotificationSettingsSerializer permission_classes = [permissions.IsAuthenticated] + def get_queryset(self): + return super().get_queryset().filter(user=self.request.user) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + class PluginDataViewSet(viewsets.ModelViewSet): - """Lists all PluginDatas in the system.""" + """Lists all PluginData in the system. + + Admin-only: plugin data may contain plugin credentials. + """ queryset = models.PluginData.objects.all() serializer_class = serializers.PluginDataSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [permissions.IsAdminUser] +@extend_schema(responses=serializers.SystemStatusSerializer) @api_view(["GET"]) @permission_classes([permissions.DashboardViewer]) def system_status(request): @@ -219,6 +235,7 @@ def system_status(request): return Response(serializer.data) +@extend_schema(request=serializers.LoginSerializer, responses=serializers.CurrentUserSerializer) @api_view(["POST"]) @authentication_classes(()) @permission_classes(()) @@ -230,12 +247,14 @@ def login(request): return Response(serializers.CurrentUserSerializer(user).data) +@extend_schema(request=None, responses=OpenApiTypes.BOOL) @api_view(["POST"]) def logout(request): auth_logout(request) return Response(True) +@extend_schema(responses=serializers.CurrentUserSerializer) @api_view(["GET"]) def current_user(request): user = request.user diff --git a/pykeg/config.py b/pykeg/config.py index c18c0c543..b6a04026c 100644 --- a/pykeg/config.py +++ b/pykeg/config.py @@ -145,10 +145,3 @@ def validate(exit_on_error=True): define_setting("KEGBOT_MEDIA_URL", default="", required=False) define_setting("KEGBOT_INSECURE_SHARED_API_KEY", default="", required=False) - -define_setting( - "KEGBOT_ENABLE_V2_API", - default=True if IS_RUNNING_PYTEST else False, - typefn=boolstr, - required=False, -) diff --git a/pykeg/contrib/webhook/plugin.py b/pykeg/contrib/webhook/plugin.py index 2a58b1717..f3d9fd7e9 100644 --- a/pykeg/contrib/webhook/plugin.py +++ b/pykeg/contrib/webhook/plugin.py @@ -2,7 +2,7 @@ from pykeg.core.util import SuppressTaskErrors from pykeg.plugin import plugin -from pykeg.proto import protolib +from pykeg.web.api import serialize from . import forms, tasks, views @@ -27,7 +27,7 @@ def handle_event(self, event): self.logger.info(f"Handling new event: {event.id}") settings = self.get_site_settings() urls = settings.get("webhook_urls", "").strip().split() - event_dict = protolib.ToDict(event, full=True) + event_dict = serialize.to_dict(event, full=True) for url in urls: with SuppressTaskErrors(self.logger): tasks.webhook_post.delay(url, event_dict) diff --git a/pykeg/proto/Makefile b/pykeg/proto/Makefile deleted file mode 100644 index da881f350..000000000 --- a/pykeg/proto/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -all: python - -python: - protoc --python_out=. *.proto - -.PHONY: python - -# vim: noet diff --git a/pykeg/proto/README.md b/pykeg/proto/README.md deleted file mode 100644 index b203ee38f..000000000 --- a/pykeg/proto/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Kegbot `proto` files - -Ages ago, we selected [Protocol -Buffers](https://developers.google.com/protocol-buffers) to define Kegbot's -core models and aspects of the rest API. This directory stores the proto -definitions and the python generated code. - -This code has been migrated from the [kegbot-api -repo](https://github.com/kegbot/kegbot-api) which is now deprecated. - -## Other clients - -At time of writing, the [Kegbot Android -App](https://github.com/kegbot/kegbot-android) also uses these protobuf -definitions. diff --git a/pykeg/proto/__init__.py b/pykeg/proto/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/pykeg/proto/api.proto b/pykeg/proto/api.proto deleted file mode 100644 index 7154e9459..000000000 --- a/pykeg/proto/api.proto +++ /dev/null @@ -1,144 +0,0 @@ -syntax = "proto2"; -option java_package = "org.kegbot.proto"; -option optimize_for = CODE_SIZE; - -import "models.proto"; - -// Common - -message Meta { - - // The total number of records available for this request. - optional uint32 total = 1; - - // The maximum number of records returned in this request. - optional uint32 limit = 2; - - // The position of the first record returned, among "total". - optional uint32 pos = 3; -} - -// Requests - -message UserRegistrationRequest { - - // Desired username. - required string username = 1; - - // User's e-mail address. - required string email = 2; - - // Initial password for logging in. If unspecified, the account will be - // registered with a random password, which can be e-mailed to the user. - optional string password = 3; - - // One of "male", "female". - optional string gender = 4; - - // Twitter username. - optional string twitter_name = 5; -} - -// Message used for recording a new drink on the backend. -message RecordDrinkRequest { - - // Name of the tap on which this drink was poured. This information is - // required, but this field may be omitted when the tap name is given - // elsewhere. (In the Kegweb API, the tap_name is part of the URL receiving - // the POST.) - optional string tap_name = 1; - - // The number of ticks, as reported by the flowmeter. Required. - required uint32 ticks = 2; - - // The volume of the pour. If unspecified, the backend will use the current - // tap configuration to compute the pour's volume based on the value of - // "ticks". - optional float volume_ml = 3; - - // The username responsible for the pour. If unspecified, this pour is - // treated as an anonymous pour. - optional string username = 4; - - // The date and time of the pour, in seconds before "now", where "now" is the - // current time on the backend at the time the request is processed. If this - // field is unspecified, a default value of "0" (meaning "now") is assumed. - // This value is ignored if "record_date" is specified. - optional uint32 seconds_ago = 5 [default = 0]; - - // The absolute date and time of the pour, as an ISO8061 UTC timestamp. If - // specified and valid, this value supercedes any value given for - // "seconds_ago", which will be ignored. - optional string record_date = 6; - - // The time taken, in seconds, to complete the pour. - optional uint32 duration_seconds = 7; - - // The authentication token used to pour the drink. - optional string auth_token = 8; - - // If true, the pour is recorded as "spilled": no drink record will be - // generated, and all fields other than "tap_name" and the volume ("ticks", or - // "volume_ml" if given) are ignored. The volume will be added to the - // spilled total for the tap’s current keg. - optional bool spilled = 9; - - // Optional message from the user about the pour. - optional string shout = 10; - - // See Drink model. - optional string tick_time_series = 11; -} - -// Message used for recording a temperature sensor reading on the backend. -message RecordTemperatureRequest { - - // The name of the sensor, as stored in the backend. - required string sensor_name = 1; - - // The observed temperature, in degress centigrade. - required float temp_c = 2; - - // The date of the reading, as an ISO8601 UTC timestamp. If this field is - // unspecified, the record_date will be the current time on the backend at the - // time the request is processed. - optional string record_date = 3; -} - -// Responses - -message SyncResponse { - // All configured controllers. - repeated Controller controllers = 1; - - // Recently-poured drinks. - repeated Drink drinks = 2; - - // Recent system events. - repeated SystemEvent events = 3; - - // Kegs on tap. - repeated Keg active_kegs = 4; - - // All configured meters. - repeated FlowMeter meters = 5; - - // Site info. - optional SiteInfo site_info = 6; - - // Sound events. - repeated SoundEvent sound_events = 7; - - // All taps. - repeated KegTap taps = 8; - - // All toggles. - repeated FlowToggle toggles = 9; - - // Current session (if there is one). - optional Session active_session = 10; - - // Currently-active users (if any). - repeated User active_users = 11; - -} diff --git a/pykeg/proto/api_pb2.py b/pykeg/proto/api_pb2.py deleted file mode 100644 index 303dea20b..000000000 --- a/pykeg/proto/api_pb2.py +++ /dev/null @@ -1,97 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: api.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database - -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from . import models_pb2 as models__pb2 - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n\tapi.proto\x1a\x0cmodels.proto"1\n\x04Meta\x12\r\n\x05total\x18\x01 \x01(\r\x12\r\n\x05limit\x18\x02 \x01(\r\x12\x0b\n\x03pos\x18\x03 \x01(\r"r\n\x17UserRegistrationRequest\x12\x10\n\x08username\x18\x01 \x02(\t\x12\r\n\x05\x65mail\x18\x02 \x02(\t\x12\x10\n\x08password\x18\x03 \x01(\t\x12\x0e\n\x06gender\x18\x04 \x01(\t\x12\x14\n\x0ctwitter_name\x18\x05 \x01(\t"\xef\x01\n\x12RecordDrinkRequest\x12\x10\n\x08tap_name\x18\x01 \x01(\t\x12\r\n\x05ticks\x18\x02 \x02(\r\x12\x11\n\tvolume_ml\x18\x03 \x01(\x02\x12\x10\n\x08username\x18\x04 \x01(\t\x12\x16\n\x0bseconds_ago\x18\x05 \x01(\r:\x01\x30\x12\x13\n\x0brecord_date\x18\x06 \x01(\t\x12\x18\n\x10\x64uration_seconds\x18\x07 \x01(\r\x12\x12\n\nauth_token\x18\x08 \x01(\t\x12\x0f\n\x07spilled\x18\t \x01(\x08\x12\r\n\x05shout\x18\n \x01(\t\x12\x18\n\x10tick_time_series\x18\x0b \x01(\t"T\n\x18RecordTemperatureRequest\x12\x13\n\x0bsensor_name\x18\x01 \x02(\t\x12\x0e\n\x06temp_c\x18\x02 \x02(\x02\x12\x13\n\x0brecord_date\x18\x03 \x01(\t"\xd2\x02\n\x0cSyncResponse\x12 \n\x0b\x63ontrollers\x18\x01 \x03(\x0b\x32\x0b.Controller\x12\x16\n\x06\x64rinks\x18\x02 \x03(\x0b\x32\x06.Drink\x12\x1c\n\x06\x65vents\x18\x03 \x03(\x0b\x32\x0c.SystemEvent\x12\x19\n\x0b\x61\x63tive_kegs\x18\x04 \x03(\x0b\x32\x04.Keg\x12\x1a\n\x06meters\x18\x05 \x03(\x0b\x32\n.FlowMeter\x12\x1c\n\tsite_info\x18\x06 \x01(\x0b\x32\t.SiteInfo\x12!\n\x0csound_events\x18\x07 \x03(\x0b\x32\x0b.SoundEvent\x12\x15\n\x04taps\x18\x08 \x03(\x0b\x32\x07.KegTap\x12\x1c\n\x07toggles\x18\t \x03(\x0b\x32\x0b.FlowToggle\x12 \n\x0e\x61\x63tive_session\x18\n \x01(\x0b\x32\x08.Session\x12\x1b\n\x0c\x61\x63tive_users\x18\x0b \x03(\x0b\x32\x05.UserB\x14\n\x10org.kegbot.protoH\x02' -) - - -_META = DESCRIPTOR.message_types_by_name["Meta"] -_USERREGISTRATIONREQUEST = DESCRIPTOR.message_types_by_name["UserRegistrationRequest"] -_RECORDDRINKREQUEST = DESCRIPTOR.message_types_by_name["RecordDrinkRequest"] -_RECORDTEMPERATUREREQUEST = DESCRIPTOR.message_types_by_name["RecordTemperatureRequest"] -_SYNCRESPONSE = DESCRIPTOR.message_types_by_name["SyncResponse"] -Meta = _reflection.GeneratedProtocolMessageType( - "Meta", - (_message.Message,), - { - "DESCRIPTOR": _META, - "__module__": "api_pb2" - # @@protoc_insertion_point(class_scope:Meta) - }, -) -_sym_db.RegisterMessage(Meta) - -UserRegistrationRequest = _reflection.GeneratedProtocolMessageType( - "UserRegistrationRequest", - (_message.Message,), - { - "DESCRIPTOR": _USERREGISTRATIONREQUEST, - "__module__": "api_pb2" - # @@protoc_insertion_point(class_scope:UserRegistrationRequest) - }, -) -_sym_db.RegisterMessage(UserRegistrationRequest) - -RecordDrinkRequest = _reflection.GeneratedProtocolMessageType( - "RecordDrinkRequest", - (_message.Message,), - { - "DESCRIPTOR": _RECORDDRINKREQUEST, - "__module__": "api_pb2" - # @@protoc_insertion_point(class_scope:RecordDrinkRequest) - }, -) -_sym_db.RegisterMessage(RecordDrinkRequest) - -RecordTemperatureRequest = _reflection.GeneratedProtocolMessageType( - "RecordTemperatureRequest", - (_message.Message,), - { - "DESCRIPTOR": _RECORDTEMPERATUREREQUEST, - "__module__": "api_pb2" - # @@protoc_insertion_point(class_scope:RecordTemperatureRequest) - }, -) -_sym_db.RegisterMessage(RecordTemperatureRequest) - -SyncResponse = _reflection.GeneratedProtocolMessageType( - "SyncResponse", - (_message.Message,), - { - "DESCRIPTOR": _SYNCRESPONSE, - "__module__": "api_pb2" - # @@protoc_insertion_point(class_scope:SyncResponse) - }, -) -_sym_db.RegisterMessage(SyncResponse) - -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b"\n\020org.kegbot.protoH\002" - _META._serialized_start = 27 - _META._serialized_end = 76 - _USERREGISTRATIONREQUEST._serialized_start = 78 - _USERREGISTRATIONREQUEST._serialized_end = 192 - _RECORDDRINKREQUEST._serialized_start = 195 - _RECORDDRINKREQUEST._serialized_end = 434 - _RECORDTEMPERATUREREQUEST._serialized_start = 436 - _RECORDTEMPERATUREREQUEST._serialized_end = 520 - _SYNCRESPONSE._serialized_start = 523 - _SYNCRESPONSE._serialized_end = 861 -# @@protoc_insertion_point(module_scope) diff --git a/pykeg/proto/kbapi.py b/pykeg/proto/kbapi.py deleted file mode 100644 index fbff98164..000000000 --- a/pykeg/proto/kbapi.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Kegweb API exceptions. - -These are re-exported here so server code can `from pykeg.proto import kbapi` -and reference e.g. `kbapi.NoAuthTokenError`. The old gflags-based HTTP client -lived here too; it was unused by the server and now lives in the kegbot-api -project. -""" - -from .exceptions import ( - BadApiKeyError, - BadRequestError, - Error, - ErrorCodeToException, - NoAuthTokenError, - NotFoundError, - PermissionDeniedError, - RequestError, - ServerError, -) - -__all__ = [ - "BadApiKeyError", - "BadRequestError", - "Error", - "ErrorCodeToException", - "NoAuthTokenError", - "NotFoundError", - "PermissionDeniedError", - "RequestError", - "ServerError", -] diff --git a/pykeg/proto/models.proto b/pykeg/proto/models.proto deleted file mode 100644 index b6071bae1..000000000 --- a/pykeg/proto/models.proto +++ /dev/null @@ -1,620 +0,0 @@ -syntax = "proto2"; -option java_package = "org.kegbot.proto"; -option optimize_for = CODE_SIZE; - -// An authentication token, which a drinker uses to authenticate to the system. -message AuthenticationToken { - // The unique identifier for this token. - required uint32 id = 1; - - // The name of the auth device that owns this token, such as ``core.onewire`` - // or ``contrib.phidget.rfid``. - required string auth_device = 2; - - // The unique key. - required string token_value = 3; - - // The user owning the token. - optional string username = 4; - - // An optional human-readable name for this token. - optional string nice_name = 5; - - // True if the token is enabled. - optional bool enabled = 6; - - // The date the token was created, as an ISO8061 UTC timestamp. - required string created_time = 7; - - // The date after which the token is invalid, as an ISO8061 UTC timestamp. - // Only available to admin users. - optional string expire_time = 8; - - // The token's pin, if any. - // Only available to admin users. - optional string pin = 9; - - optional User user = 10; -} - -// A beverage production company or brand. -message BeverageProducer { - - // The unique identifier for this producer. - required uint32 id = 1; - - // The name of the brewer or production company. - required string name = 2; - - // Country of origin. - optional string country = 3; - - // State of origin. - optional string origin_state = 4; - - // City of origin. - optional string origin_city = 5; - - // True if this is a home producer. - optional bool is_homebrew = 6; - - // URL for producer. - optional string url = 7; - - // Free-form description. - optional string description = 8; - - // Image. - optional Image picture = 9; - -} - -// A type of beer, coffee, wine, or other beverage. -message Beverage { - - // Unique id for this beverage. - required uint32 id = 1; - - // Beverage name. - required string name = 2; - - // Producer. - required BeverageProducer producer = 3; - - // Type of beverage. - required string beverage_type = 4; - - // Beverage style. - optional string style = 5; - - // Free-form description. - optional string description = 6; - - // Logo or other image. - optional Image picture = 7; - - // Vintage. - optional uint32 vintage_year = 8; - - // Alcohol by volume (0.0-100.0). - optional double abv_percent = 9; - - // Calories. - optional double calories_per_ml = 10; - - // Carbohydrates. - optional double carbs_per_ml = 11; - - // Original gravity. - optional double original_gravity = 12; - - // Specific gravity. - optional double specific_gravity = 13; - - // Untappd id (for beers only). - optional string untappd_id = 14; - - // Beverage backend. - optional string beverage_backend = 15; - - // Id within beverage backend. - optional string beverage_backend_id = 16; - - // v1.1 fields - - optional string color_hex = 17; - optional double srm = 18; - optional double ibu = 19; - optional double star_rating = 20; - -} - -// A named beer style of beer, such as "India Pale Ale". -message BeerStyle { - - // The unique identifier for this beer style. - required uint32 id = 1; - - // The name of the beer style. - required string name = 2; - -} - -// A specific kind of beer: describes the beer's name, style, and brewer. -message BeerType { - - // The unique identifier for this beer type. - required string id = 1; - - // The brand name of the beer. - required string name = 2; - - // Brewer information for the beer. May refer to an 'unknown' or generic - // brewer record. - required string brewer_id = 3; - - // Style information for the beer. May refer to an 'unknown' or generic beer - // style. - required string style_id = 4; - - // For seasonal or special edition beers, the year or other edition name. - optional string edition = 6; - - // Alcohol by volume, as a percentage of the total volume. - optional double abv = 7; - - // Number of calories per ounce of beverage. - optional double calories_oz = 8; - - // Number of carbohydrates per ounce of beverage. - optional double carbs_oz = 9; - - // Specific/final gravity of the beer, if known. - optional double specific_gravity = 10; - - // Original gravity of the beer, if known. - optional double original_gravity = 11; - - // Image for this beer. - optional Image image = 12; - -} - -// A specific producer of our favorite beverage. -message Brewer { - - // The unique identifier for this beer type. - required string id = 1; - - // The name of the brewer. - required string name = 2; - - // The country of the brewer's headquarters. - optional string country = 3 [default = '']; - - // The state of the brewer's headquarters. - optional string origin_state = 4 [default = '']; - - // The city of the brewer's headquarters. - optional string origin_city = 5 [default = '']; - - // Type of production (usually either 'retail' or 'homebrew'). - optional string production = 6 [default = '']; - - // URL of brewer. - optional string url = 7 [default = '']; - - // Free-form description. - optional string description = 8 [default = '']; - - // Image or logo for this brewer. - optional Image image = 9; - -} - -// Describes a single recorded pour from the Kegbot. -message Drink { - - // The unique identifier for this drink. - required uint32 id = 1; - - // The number of meter ticks recorded for the drink. - required uint32 ticks = 2; - - // The volume of the drink, in milliliters. - required double volume_ml = 3; - - // The session this drink belongs to. - required uint32 session_id = 4; - - // UTC time when the drink was poured, as an ISO8061 UTC timestamp. - required string time = 5; - - // Duration, in seconds, of the pour. - optional uint32 duration = 6; - - // The Keg from which the drink was poured. May be unset if the drink was - // not associated with a keg. - optional uint32 keg_id = 8; - - // The User that poured the drink. Snset if the drinker was unknown - // (anonymous pour). - optional string user_id = 9; - - // Auth token value used to pour the drink, if known. - optional uint32 auth_token_id = 10; - - // Canonical URL for this object. - optional string url = 11; - - // Comment from the drinker at the time of the pour. - optional string shout = 12; - - optional User user = 13; - optional Keg keg = 14; - optional Session session = 15; - repeated Image images = 16; - - // Vector of tick updates. Format is: "