init comment-service

This commit is contained in:
2023-09-27 16:05:24 +01:00
parent 0ae3ee3af4
commit 55a533c461
41 changed files with 2800 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
import logging
from typing import Type
from google.protobuf import message
from aiokafka import AIOKafkaConsumer
from aiokafka.structs import ConsumerRecord
from comment_service.models.config import Config
from comment_service.models.service import CommentDBRepository
class EventConsumer:
"""An abstract consumer base class.
Attributes:
CONSUMER_TOPIC: The topic to consume events from.
CONSUMER_EVENT_TYPE (Type[message.Message]): The protobuf class type of the event msgs (used for deserialisation).
_db_repo (Type[CommentDBRepository]): The repository interface for modifying data.
_consumer (aiokafka.AIOKafkaConsumer): The underlying Kafka instance.
"""
CONSUMER_TOPIC: str
CONSUMER_EVENT_TYPE: Type[message.Message]
def __init__(self, config: Config, db_repo: Type[CommentDBRepository]) -> None:
"""Initialise the event consumer.
Args:
config (Config): The app configuration instance (to access brokers list).
db_repo (Type[CommentDBRepository]): The repository interface for updating data.
"""
self._db_repo = db_repo
self._consumer = AIOKafkaConsumer(
self.CONSUMER_TOPIC,
bootstrap_servers=config.kafka_brokers,
group_id="comment-service"
)
async def start(self) -> None:
"""Begin consuming messages."""
await self._consumer.start()
try:
async for msg in self._consumer:
await self._process_msg(msg)
except Exception as e:
logging.error(f"error whilst consuming messages (on topic '{self.CONSUMER_TOPIC}'): {e}")
finally:
await self._consumer.stop()
async def _process_msg(self, msg: ConsumerRecord) -> None:
"""Process a recieved message.
The messages are deserialise from bytes into their protobuf form,
then passed to the `_process_event` method to handle the logic.
Args:
msg (kafka.Message): The event to process.
"""
try:
event = self.CONSUMER_EVENT_TYPE()
event.ParseFromString(msg.value)
assert event.type != ""
await self._process_event(event)
except AssertionError:
logging.error("invalid event recieved")
return
except Exception as e:
logging.error("error whilst processing recieved event:", e)
return
async def _process_event(self, event: Type[message.Message]) -> None:
"""Process a recieved event.
Args:
event (Type[message.Message]): The event serialised to protobuf form.
"""
raise NotImplementedError("required consumer method (_process_event) not implemented")

View File

@@ -0,0 +1,33 @@
import logging
from comment_service.models.proto import post_pb2
from comment_service.events.base_consumer import EventConsumer
class PostEventConsumer(EventConsumer):
"""Consumer class responsible for 'post' events.
Attributes:
CONSUMER_TOPIC: The topic to consume events from.
CONSUMER_EVENT_TYPE (post_pb2.PostEvent): Kafka messages are serialised to this type.
_db_repo (CommentDBRepository): The repository interface for modifying data.
_consumer (aiokafka.AIOKafkaConsumer): The underlying Kafka instance.
"""
CONSUMER_TOPIC = "post"
CONSUMER_EVENT_TYPE = post_pb2.PostEvent
async def _process_event(self, event: post_pb2.PostEvent) -> None:
"""Process a recieved event.
In response to a Post deleted event, delete any comments
that fall under that post.
Args:
event (post_pb2.PostEvent): The decoded protobuf message.
"""
if event.type == "deleted":
assert event.data.id != ""
await self._db_repo.delete_post_comments(event.data.id)
logging.info("succesfully processed PostEvent (type: 'deleted')")

View File

@@ -0,0 +1,89 @@
import asyncio
from aiokafka import AIOKafkaProducer
from comment_service.models.config import Config
from comment_service.models.service import Comment
from comment_service.models.proto import comment_pb2
class CommentEventProducer:
"""Service event producer.
Attributes:
_producer (aiokafka.AIOKafkaProducer): The underlying Kafka instance.
_pending_sends (set): Background events that are still awaiting a response.
"""
def __init__(self, config: Config) -> None:
"""Initialise the event producer.
Args:
config (Config): The app configuration instance (to access Kafka brokers list).
"""
self._producer = AIOKafkaProducer(
bootstrap_servers=config.kafka_brokers,
client_id="auth-service"
)
self._pending_sends = set()
async def start(self) -> None:
"""Start the underlying Kafka instance (open a connection)."""
await self._producer.start()
async def _send_event(self, event: comment_pb2.CommentEvent) -> None:
"""Send an event.
Execute the sending action as a background task to avoid
delaying a response to the request.
Args:
event (comment_pb2.CommentEvent): The protobuf class for the event.
"""
bg_task = asyncio.create_task(self._producer.send(
topic="comment",
value=event.SerializeToString(),
))
self._pending_sends.add(bg_task)
bg_task.add_done_callback(self._pending_sends.discard)
async def send_created_event(self, comment: Comment) -> None:
"""Send a comment created event.
Args:
comment (Comment): The comment to reference.
"""
event = comment_pb2.CommentEvent(
type="created",
data=Comment.to_protobuf(comment)
)
await self._send_event(event)
async def send_updated_event(self, comment: Comment) -> None:
"""Send a comment updated event.
Args:
comment (Comment): The comment to reference.
"""
event = comment_pb2.CommentEvent(
type="updated",
data=Comment.to_protobuf(comment)
)
await self._send_event(event)
async def send_deleted_event(self, comment: Comment) -> None:
"""Send a comment deleted event.
Args:
comment (Comment): The comment to reference.
"""
event = comment_pb2.CommentEvent(
type="deleted",
data=Comment.to_protobuf(comment)
)
await self._send_event(event)

View File

@@ -0,0 +1,67 @@
import asyncio
from typing import Type
from comment_service.models.config import Config
from comment_service.models.service import CommentDBRepository
from comment_service.events.producer import CommentEventProducer
from comment_service.events.post_consumer import PostEventConsumer
from comment_service.events.user_consumer import UserEventConsumer
class EventConsumersWrapper:
"""A wrapper class for starting the event consumers.
Attributes:
_post_consumer (PostEventConsumer): A wrapped consumer.
_user_consumer (UserEventConsumer): A wrapped consumer.
"""
def __init__(self, post_consumer: PostEventConsumer, user_consumer: UserEventConsumer) -> None:
"""Add the consumers to the wrapper
Args:
post_consumer (PostEventConsumer): Initialised post consumer.
user_consumer (UserEventConsumer): Initialised user consumer.
"""
self._post_consumer = post_consumer
self._user_consumer = user_consumer
async def start(self) -> None:
"""Begin consuming events on all the event consumers."""
await asyncio.gather(
self._post_consumer.start(),
self._user_consumer.start()
)
def create_consumers(config: Config, db_repo: Type[CommentDBRepository]) -> EventConsumersWrapper:
"""Initialse the event consumers and return them in a wrapper.
Args:
config (Config): The app configuration instance.
db_repo (Type[CommentDBRepository]): The database repo to pass to the consumers.
Returns:
EventConsumerWrapper
"""
post_consumer = PostEventConsumer(config, db_repo)
user_consumer = UserEventConsumer(config, db_repo)
return EventConsumersWrapper(post_consumer=post_consumer, user_consumer=user_consumer)
async def create_producer(config: Config) -> CommentEventProducer:
"""Create an event producer for the service.
Args:
config (Config): The app configuration instance.
Returns:
CommentEventProducer
"""
producer = CommentEventProducer(config)
await producer.start()
return producer

View File

@@ -0,0 +1,33 @@
import logging
from comment_service.models.proto import user_pb2
from comment_service.events.base_consumer import EventConsumer
class UserEventConsumer(EventConsumer):
"""Consumer class responsible for 'user' events.
Attributes:
CONSUMER_TOPIC: The topic to consume events from.
CONSUMER_EVENT_TYPE (user_pb2.UserEvent): Kafka messages are serialised to this type.
_db_repo (CommentDBRepository): The repository interface for modifying data.
_consumer (aiokafka.AIOKafkaConsumer): The underlying Kafka instance.
"""
CONSUMER_TOPIC = "user"
CONSUMER_EVENT_TYPE = user_pb2.UserEvent
async def _process_event(self, event: user_pb2.UserEvent) -> None:
"""Process a recieved event.
In response to a User deleted event, delete any comments
created by that user.
Args:
event (user_pb2.UserEvent): The decoded protobuf message.
"""
if event.type == "deleted":
assert event.data.id != ""
await self._db_repo.delete_user_comments(event.data.id)
logging.info("succesfully processed UserEvent (type: 'deleted')")

View File

@@ -0,0 +1,32 @@
import asyncio
import logging
from sys import stdout
from comment_service.models.config import Config
from comment_service.events.service import create_producer, create_consumers
from comment_service.postgres.service import create_db_repository
from comment_service.redis.service import create_redis_repository
from comment_service.rpc.service import create_rpc_server
from comment_service.service import ServiceRepository
async def main() -> None:
config = Config()
event_prod = await create_producer(config)
db_repo = await create_db_repository(config, event_producer=event_prod)
redis_repo = await create_redis_repository(config, downstream_repo=db_repo)
svc_repo = ServiceRepository(downstream_repo=redis_repo)
rpc_server = create_rpc_server(svc_repo)
event_consumers = create_consumers(config, db_repo=db_repo)
await asyncio.gather(
rpc_server.start(),
event_consumers.start()
)
if __name__ == "__main__":
logging.basicConfig(stream=stdout, level=logging.INFO)
asyncio.run(main())

View File

@@ -0,0 +1,66 @@
from typing import Any, List
from pydantic import computed_field
from pydantic.fields import FieldInfo
from pydantic_settings import BaseSettings, EnvSettingsSource
from pydantic_settings.main import BaseSettings
from pydantic_settings.sources import PydanticBaseSettingsSource
class ConfigSource(EnvSettingsSource):
"""Responsible for loading config options from environment variables."""
def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any:
if field_name == "kafka_brokers":
# Comma delimit the kafka brokers.
if value == None:
return None
return value.split(",")
return super().prepare_field_value(field_name, field, value, value_is_complex)
class Config(BaseSettings):
"""The service configuration loaded from environment
variables.
Attributes:
postgres_user (str): Loaded from the 'POSTGRES_USER' envvar.
postgres_pass (str): Loaded from the 'POSTGRES_PASS' envvar.
postgres_host (str): Loaded from the 'POSTGRES_HOST' envvar.
postgres_database (str): Loaded from the 'POSTGRES_DATABASE' envvar.
redis_host (str): Loaded from the 'REDIS_HOST' envvar.
redis_pass (str): Loaded from the 'REDIS_PASS' envvar.
kafka_brokers (list[str]): Loaded and comma delmited from the 'KAFKA_BROKERS' envvar.
postgres_dsn (str): Computed when accessed the first time. (@property)
"""
postgres_user: str
postgres_pass: str
postgres_host: str
postgres_database: str
redis_host: str
redis_pass: str
kafka_brokers: List[str]
@computed_field
@property
def postgres_dsn(self) -> str:
"""Uses the postgres_user, postgres_pass, postgres_host,
and postgres_database options to assemble a DSN.
Returns:
str: DSN for connecting to the database.
"""
return "postgresql+asyncpg://{user}:{password}@{host}/{db}".format(
user=self.postgres_user,
password=self.postgres_pass,
host=self.postgres_host,
db=self.postgres_database
)
@classmethod
def settings_customise_sources(cls, settings_cls: type[BaseSettings], *args, **kwargs) -> tuple[PydanticBaseSettingsSource, ...]:
return (ConfigSource(settings_cls), )

View File

@@ -0,0 +1,59 @@
from enum import Enum, auto
from grpc import RpcContext, StatusCode
class ServiceErrorCode(Enum):
"""Error codes used for classifying ServiceExceptions."""
INVALID_ARGUMENT = auto()
CONFLICT = auto()
NOT_FOUND = auto()
INVALID_CREDENTIALS = auto()
SERVICE_ERROR = auto()
__RPC_CODE_MAP__ = {
INVALID_ARGUMENT: StatusCode.INVALID_ARGUMENT,
CONFLICT: StatusCode.ALREADY_EXISTS,
NOT_FOUND: StatusCode.NOT_FOUND,
INVALID_CREDENTIALS: StatusCode.UNAUTHENTICATED,
SERVICE_ERROR: StatusCode.INTERNAL
}
def to_rpc_code(self) -> StatusCode:
"""Convert a service error code to a gRPC status code.
Returns:
The mapped RPC status code, if found, otherwise gRPC Unknown status code.
"""
return self.__class__.__RPC_CODE_MAP__.get(self.value, StatusCode.UNKNOWN)
class ServiceException(Exception):
"""This exception provides an interface to convert service errors
into gRPC errors, which can then be returned to the caller.
Args:
msg (str): Error message.
error_code (ServiceErrorCode): Categorisation code for the error.
Attributes:
msg (str): The error message.
error_code (ServiceErrorCode): Categorisation code for the error.
"""
def __init__(self, msg: str, error_code: ServiceErrorCode) -> None:
super().__init__(msg)
self.msg = msg
self.error_code = error_code
def apply_to_rpc(self, context: RpcContext) -> None:
"""Apply the exception to an RPC context.
Args:
context (grpc.RpcContext): The context to apply to.
"""
context.set_code(self.error_code.to_rpc_code())
context.set_details(self.msg)

View File

@@ -0,0 +1,34 @@
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_sym_db = _symbol_database.Default()
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rcomment.proto\x12\x11panels.comment.v1\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto"\xaa\x01\n\x07Comment\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0f\n\x07post_id\x18\x02 \x01(\t\x12\x11\n\tauthor_id\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\t\x12.\n\ncreated_at\x18\x05 \x01(\x0b2\x1a.google.protobuf.Timestamp\x12.\n\nupdated_at\x18\x06 \x01(\x0b2\x1a.google.protobuf.Timestamp"!\n\x0eCommentMutable\x12\x0f\n\x07message\x18\x01 \x01(\t"k\n\x14CreateCommentRequest\x12\x0f\n\x07post_id\x18\x01 \x01(\t\x12\x11\n\tauthor_id\x18\x02 \x01(\t\x12/\n\x04data\x18\x03 \x01(\x0b2!.panels.comment.v1.CommentMutable"S\n\x14UpdateCommentRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12/\n\x04data\x18\x02 \x01(\x0b2!.panels.comment.v1.CommentMutable""\n\x14DeleteCommentRequest\x12\n\n\x02id\x18\x01 \x01(\t"\x1f\n\x11GetCommentRequest\x12\n\n\x02id\x18\x01 \x01(\t")\n\x16GetPostCommentsRequest\x12\x0f\n\x07post_id\x18\x01 \x01(\t"<\n\x0cPostComments\x12,\n\x08comments\x18\x01 \x03(\x0b2\x1a.panels.comment.v1.Comment"F\n\x0cCommentEvent\x12\x0c\n\x04type\x18\x01 \x01(\t\x12(\n\x04data\x18\x02 \x01(\x0b2\x1a.panels.comment.v1.Comment2\xc7\x03\n\x0eCommentService\x12V\n\rCreateComment\x12\'.panels.comment.v1.CreateCommentRequest\x1a\x1a.panels.comment.v1.Comment"\x00\x12V\n\rUpdateComment\x12\'.panels.comment.v1.UpdateCommentRequest\x1a\x1a.panels.comment.v1.Comment"\x00\x12R\n\rDeleteComment\x12\'.panels.comment.v1.DeleteCommentRequest\x1a\x16.google.protobuf.Empty"\x00\x12P\n\nGetComment\x12$.panels.comment.v1.GetCommentRequest\x1a\x1a.panels.comment.v1.Comment"\x00\x12_\n\x0fGetPostComments\x12).panels.comment.v1.GetPostCommentsRequest\x1a\x1f.panels.comment.v1.PostComments"\x00b\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'comment_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_globals['_COMMENT']._serialized_start = 99
_globals['_COMMENT']._serialized_end = 269
_globals['_COMMENTMUTABLE']._serialized_start = 271
_globals['_COMMENTMUTABLE']._serialized_end = 304
_globals['_CREATECOMMENTREQUEST']._serialized_start = 306
_globals['_CREATECOMMENTREQUEST']._serialized_end = 413
_globals['_UPDATECOMMENTREQUEST']._serialized_start = 415
_globals['_UPDATECOMMENTREQUEST']._serialized_end = 498
_globals['_DELETECOMMENTREQUEST']._serialized_start = 500
_globals['_DELETECOMMENTREQUEST']._serialized_end = 534
_globals['_GETCOMMENTREQUEST']._serialized_start = 536
_globals['_GETCOMMENTREQUEST']._serialized_end = 567
_globals['_GETPOSTCOMMENTSREQUEST']._serialized_start = 569
_globals['_GETPOSTCOMMENTSREQUEST']._serialized_end = 610
_globals['_POSTCOMMENTS']._serialized_start = 612
_globals['_POSTCOMMENTS']._serialized_end = 672
_globals['_COMMENTEVENT']._serialized_start = 674
_globals['_COMMENTEVENT']._serialized_end = 744
_globals['_COMMENTSERVICE']._serialized_start = 747
_globals['_COMMENTSERVICE']._serialized_end = 1202

View File

@@ -0,0 +1,97 @@
from google.protobuf import empty_pb2 as _empty_pb2
from google.protobuf import timestamp_pb2 as _timestamp_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class Comment(_message.Message):
__slots__ = ['id', 'post_id', 'author_id', 'message', 'created_at', 'updated_at']
ID_FIELD_NUMBER: _ClassVar[int]
POST_ID_FIELD_NUMBER: _ClassVar[int]
AUTHOR_ID_FIELD_NUMBER: _ClassVar[int]
MESSAGE_FIELD_NUMBER: _ClassVar[int]
CREATED_AT_FIELD_NUMBER: _ClassVar[int]
UPDATED_AT_FIELD_NUMBER: _ClassVar[int]
id: str
post_id: str
author_id: str
message: str
created_at: _timestamp_pb2.Timestamp
updated_at: _timestamp_pb2.Timestamp
def __init__(self, id: _Optional[str]=..., post_id: _Optional[str]=..., author_id: _Optional[str]=..., message: _Optional[str]=..., created_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]]=..., updated_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]]=...) -> None:
...
class CommentMutable(_message.Message):
__slots__ = ['message']
MESSAGE_FIELD_NUMBER: _ClassVar[int]
message: str
def __init__(self, message: _Optional[str]=...) -> None:
...
class CreateCommentRequest(_message.Message):
__slots__ = ['post_id', 'author_id', 'data']
POST_ID_FIELD_NUMBER: _ClassVar[int]
AUTHOR_ID_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
post_id: str
author_id: str
data: CommentMutable
def __init__(self, post_id: _Optional[str]=..., author_id: _Optional[str]=..., data: _Optional[_Union[CommentMutable, _Mapping]]=...) -> None:
...
class UpdateCommentRequest(_message.Message):
__slots__ = ['id', 'data']
ID_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
id: str
data: CommentMutable
def __init__(self, id: _Optional[str]=..., data: _Optional[_Union[CommentMutable, _Mapping]]=...) -> None:
...
class DeleteCommentRequest(_message.Message):
__slots__ = ['id']
ID_FIELD_NUMBER: _ClassVar[int]
id: str
def __init__(self, id: _Optional[str]=...) -> None:
...
class GetCommentRequest(_message.Message):
__slots__ = ['id']
ID_FIELD_NUMBER: _ClassVar[int]
id: str
def __init__(self, id: _Optional[str]=...) -> None:
...
class GetPostCommentsRequest(_message.Message):
__slots__ = ['post_id']
POST_ID_FIELD_NUMBER: _ClassVar[int]
post_id: str
def __init__(self, post_id: _Optional[str]=...) -> None:
...
class PostComments(_message.Message):
__slots__ = ['comments']
COMMENTS_FIELD_NUMBER: _ClassVar[int]
comments: _containers.RepeatedCompositeFieldContainer[Comment]
def __init__(self, comments: _Optional[_Iterable[_Union[Comment, _Mapping]]]=...) -> None:
...
class CommentEvent(_message.Message):
__slots__ = ['type', 'data']
TYPE_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
type: str
data: Comment
def __init__(self, type: _Optional[str]=..., data: _Optional[_Union[Comment, _Mapping]]=...) -> None:
...

View File

@@ -0,0 +1,80 @@
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
from . import comment_pb2 as comment__pb2
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
class CommentServiceStub(object):
"""Missing associated documentation comment in .proto file."""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.CreateComment = channel.unary_unary('/panels.comment.v1.CommentService/CreateComment', request_serializer=comment__pb2.CreateCommentRequest.SerializeToString, response_deserializer=comment__pb2.Comment.FromString)
self.UpdateComment = channel.unary_unary('/panels.comment.v1.CommentService/UpdateComment', request_serializer=comment__pb2.UpdateCommentRequest.SerializeToString, response_deserializer=comment__pb2.Comment.FromString)
self.DeleteComment = channel.unary_unary('/panels.comment.v1.CommentService/DeleteComment', request_serializer=comment__pb2.DeleteCommentRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString)
self.GetComment = channel.unary_unary('/panels.comment.v1.CommentService/GetComment', request_serializer=comment__pb2.GetCommentRequest.SerializeToString, response_deserializer=comment__pb2.Comment.FromString)
self.GetPostComments = channel.unary_unary('/panels.comment.v1.CommentService/GetPostComments', request_serializer=comment__pb2.GetPostCommentsRequest.SerializeToString, response_deserializer=comment__pb2.PostComments.FromString)
class CommentServiceServicer(object):
"""Missing associated documentation comment in .proto file."""
def CreateComment(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def UpdateComment(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def DeleteComment(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetComment(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetPostComments(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_CommentServiceServicer_to_server(servicer, server):
rpc_method_handlers = {'CreateComment': grpc.unary_unary_rpc_method_handler(servicer.CreateComment, request_deserializer=comment__pb2.CreateCommentRequest.FromString, response_serializer=comment__pb2.Comment.SerializeToString), 'UpdateComment': grpc.unary_unary_rpc_method_handler(servicer.UpdateComment, request_deserializer=comment__pb2.UpdateCommentRequest.FromString, response_serializer=comment__pb2.Comment.SerializeToString), 'DeleteComment': grpc.unary_unary_rpc_method_handler(servicer.DeleteComment, request_deserializer=comment__pb2.DeleteCommentRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString), 'GetComment': grpc.unary_unary_rpc_method_handler(servicer.GetComment, request_deserializer=comment__pb2.GetCommentRequest.FromString, response_serializer=comment__pb2.Comment.SerializeToString), 'GetPostComments': grpc.unary_unary_rpc_method_handler(servicer.GetPostComments, request_deserializer=comment__pb2.GetPostCommentsRequest.FromString, response_serializer=comment__pb2.PostComments.SerializeToString)}
generic_handler = grpc.method_handlers_generic_handler('panels.comment.v1.CommentService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
class CommentService(object):
"""Missing associated documentation comment in .proto file."""
@staticmethod
def CreateComment(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.comment.v1.CommentService/CreateComment', comment__pb2.CreateCommentRequest.SerializeToString, comment__pb2.Comment.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def UpdateComment(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.comment.v1.CommentService/UpdateComment', comment__pb2.UpdateCommentRequest.SerializeToString, comment__pb2.Comment.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def DeleteComment(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.comment.v1.CommentService/DeleteComment', comment__pb2.DeleteCommentRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def GetComment(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.comment.v1.CommentService/GetComment', comment__pb2.GetCommentRequest.SerializeToString, comment__pb2.Comment.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def GetPostComments(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.comment.v1.CommentService/GetPostComments', comment__pb2.GetPostCommentsRequest.SerializeToString, comment__pb2.PostComments.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

View File

@@ -0,0 +1,44 @@
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_sym_db = _symbol_database.Default()
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\npost.proto\x12\x0epanels.post.v1\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto"\xb7\x01\n\x04Post\x12\n\n\x02id\x18\x01 \x01(\t\x12\x10\n\x08panel_id\x18\x02 \x01(\t\x12\x11\n\tauthor_id\x18\x03 \x01(\t\x12\r\n\x05title\x18\x04 \x01(\t\x12\x0f\n\x07content\x18\x05 \x01(\t\x12.\n\ncreated_at\x18\x06 \x01(\x0b2\x1a.google.protobuf.Timestamp\x12.\n\nupdated_at\x18\x07 \x01(\x0b2\x1a.google.protobuf.Timestamp"M\n\x0bPostMutable\x12\x12\n\x05title\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x14\n\x07content\x18\x02 \x01(\tH\x01\x88\x01\x01B\x08\n\x06_titleB\n\n\x08_content"a\n\x11CreatePostRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\x12)\n\x04data\x18\x03 \x01(\x0b2\x1b.panels.post.v1.PostMutable"\x1c\n\x0eGetPostRequest\x12\n\n\x02id\x18\x01 \x01(\t"3\n\x13GetPanelPostRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\t"J\n\x11UpdatePostRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12)\n\x04data\x18\x02 \x01(\x0b2\x1b.panels.post.v1.PostMutable"\x1f\n\x11DeletePostRequest\x12\n\n\x02id\x18\x01 \x01(\t"\x15\n\x13GetFeedPostsRequest"0\n\tFeedPosts\x12#\n\x05posts\x18\x01 \x03(\x0b2\x14.panels.post.v1.Post"&\n\x13GetUserPostsRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t"0\n\tUserPosts\x12#\n\x05posts\x18\x01 \x03(\x0b2\x14.panels.post.v1.Post"(\n\x14GetPanelPostsRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t"1\n\nPanelPosts\x12#\n\x05posts\x18\x01 \x03(\x0b2\x14.panels.post.v1.Post"=\n\tPostEvent\x12\x0c\n\x04type\x18\x01 \x01(\t\x12"\n\x04data\x18\x02 \x01(\x0b2\x14.panels.post.v1.Post2\xf3\x04\n\x0bPostService\x12G\n\nCreatePost\x12!.panels.post.v1.CreatePostRequest\x1a\x14.panels.post.v1.Post"\x00\x12A\n\x07GetPost\x12\x1e.panels.post.v1.GetPostRequest\x1a\x14.panels.post.v1.Post"\x00\x12K\n\x0cGetPanelPost\x12#.panels.post.v1.GetPanelPostRequest\x1a\x14.panels.post.v1.Post"\x00\x12G\n\nUpdatePost\x12!.panels.post.v1.UpdatePostRequest\x1a\x14.panels.post.v1.Post"\x00\x12I\n\nDeletePost\x12!.panels.post.v1.DeletePostRequest\x1a\x16.google.protobuf.Empty"\x00\x12P\n\x0cGetFeedPosts\x12#.panels.post.v1.GetFeedPostsRequest\x1a\x19.panels.post.v1.FeedPosts"\x00\x12P\n\x0cGetUserPosts\x12#.panels.post.v1.GetUserPostsRequest\x1a\x19.panels.post.v1.UserPosts"\x00\x12S\n\rGetPanelPosts\x12$.panels.post.v1.GetPanelPostsRequest\x1a\x1a.panels.post.v1.PanelPosts"\x00b\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'post_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_globals['_POST']._serialized_start = 93
_globals['_POST']._serialized_end = 276
_globals['_POSTMUTABLE']._serialized_start = 278
_globals['_POSTMUTABLE']._serialized_end = 355
_globals['_CREATEPOSTREQUEST']._serialized_start = 357
_globals['_CREATEPOSTREQUEST']._serialized_end = 454
_globals['_GETPOSTREQUEST']._serialized_start = 456
_globals['_GETPOSTREQUEST']._serialized_end = 484
_globals['_GETPANELPOSTREQUEST']._serialized_start = 486
_globals['_GETPANELPOSTREQUEST']._serialized_end = 537
_globals['_UPDATEPOSTREQUEST']._serialized_start = 539
_globals['_UPDATEPOSTREQUEST']._serialized_end = 613
_globals['_DELETEPOSTREQUEST']._serialized_start = 615
_globals['_DELETEPOSTREQUEST']._serialized_end = 646
_globals['_GETFEEDPOSTSREQUEST']._serialized_start = 648
_globals['_GETFEEDPOSTSREQUEST']._serialized_end = 669
_globals['_FEEDPOSTS']._serialized_start = 671
_globals['_FEEDPOSTS']._serialized_end = 719
_globals['_GETUSERPOSTSREQUEST']._serialized_start = 721
_globals['_GETUSERPOSTSREQUEST']._serialized_end = 759
_globals['_USERPOSTS']._serialized_start = 761
_globals['_USERPOSTS']._serialized_end = 809
_globals['_GETPANELPOSTSREQUEST']._serialized_start = 811
_globals['_GETPANELPOSTSREQUEST']._serialized_end = 851
_globals['_PANELPOSTS']._serialized_start = 853
_globals['_PANELPOSTS']._serialized_end = 902
_globals['_POSTEVENT']._serialized_start = 904
_globals['_POSTEVENT']._serialized_end = 965
_globals['_POSTSERVICE']._serialized_start = 968
_globals['_POSTSERVICE']._serialized_end = 1595

View File

@@ -0,0 +1,141 @@
from google.protobuf import empty_pb2 as _empty_pb2
from google.protobuf import timestamp_pb2 as _timestamp_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class Post(_message.Message):
__slots__ = ['id', 'panel_id', 'author_id', 'title', 'content', 'created_at', 'updated_at']
ID_FIELD_NUMBER: _ClassVar[int]
PANEL_ID_FIELD_NUMBER: _ClassVar[int]
AUTHOR_ID_FIELD_NUMBER: _ClassVar[int]
TITLE_FIELD_NUMBER: _ClassVar[int]
CONTENT_FIELD_NUMBER: _ClassVar[int]
CREATED_AT_FIELD_NUMBER: _ClassVar[int]
UPDATED_AT_FIELD_NUMBER: _ClassVar[int]
id: str
panel_id: str
author_id: str
title: str
content: str
created_at: _timestamp_pb2.Timestamp
updated_at: _timestamp_pb2.Timestamp
def __init__(self, id: _Optional[str]=..., panel_id: _Optional[str]=..., author_id: _Optional[str]=..., title: _Optional[str]=..., content: _Optional[str]=..., created_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]]=..., updated_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]]=...) -> None:
...
class PostMutable(_message.Message):
__slots__ = ['title', 'content']
TITLE_FIELD_NUMBER: _ClassVar[int]
CONTENT_FIELD_NUMBER: _ClassVar[int]
title: str
content: str
def __init__(self, title: _Optional[str]=..., content: _Optional[str]=...) -> None:
...
class CreatePostRequest(_message.Message):
__slots__ = ['panel_id', 'user_id', 'data']
PANEL_ID_FIELD_NUMBER: _ClassVar[int]
USER_ID_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
panel_id: str
user_id: str
data: PostMutable
def __init__(self, panel_id: _Optional[str]=..., user_id: _Optional[str]=..., data: _Optional[_Union[PostMutable, _Mapping]]=...) -> None:
...
class GetPostRequest(_message.Message):
__slots__ = ['id']
ID_FIELD_NUMBER: _ClassVar[int]
id: str
def __init__(self, id: _Optional[str]=...) -> None:
...
class GetPanelPostRequest(_message.Message):
__slots__ = ['panel_id', 'id']
PANEL_ID_FIELD_NUMBER: _ClassVar[int]
ID_FIELD_NUMBER: _ClassVar[int]
panel_id: str
id: str
def __init__(self, panel_id: _Optional[str]=..., id: _Optional[str]=...) -> None:
...
class UpdatePostRequest(_message.Message):
__slots__ = ['id', 'data']
ID_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
id: str
data: PostMutable
def __init__(self, id: _Optional[str]=..., data: _Optional[_Union[PostMutable, _Mapping]]=...) -> None:
...
class DeletePostRequest(_message.Message):
__slots__ = ['id']
ID_FIELD_NUMBER: _ClassVar[int]
id: str
def __init__(self, id: _Optional[str]=...) -> None:
...
class GetFeedPostsRequest(_message.Message):
__slots__ = []
def __init__(self) -> None:
...
class FeedPosts(_message.Message):
__slots__ = ['posts']
POSTS_FIELD_NUMBER: _ClassVar[int]
posts: _containers.RepeatedCompositeFieldContainer[Post]
def __init__(self, posts: _Optional[_Iterable[_Union[Post, _Mapping]]]=...) -> None:
...
class GetUserPostsRequest(_message.Message):
__slots__ = ['user_id']
USER_ID_FIELD_NUMBER: _ClassVar[int]
user_id: str
def __init__(self, user_id: _Optional[str]=...) -> None:
...
class UserPosts(_message.Message):
__slots__ = ['posts']
POSTS_FIELD_NUMBER: _ClassVar[int]
posts: _containers.RepeatedCompositeFieldContainer[Post]
def __init__(self, posts: _Optional[_Iterable[_Union[Post, _Mapping]]]=...) -> None:
...
class GetPanelPostsRequest(_message.Message):
__slots__ = ['panel_id']
PANEL_ID_FIELD_NUMBER: _ClassVar[int]
panel_id: str
def __init__(self, panel_id: _Optional[str]=...) -> None:
...
class PanelPosts(_message.Message):
__slots__ = ['posts']
POSTS_FIELD_NUMBER: _ClassVar[int]
posts: _containers.RepeatedCompositeFieldContainer[Post]
def __init__(self, posts: _Optional[_Iterable[_Union[Post, _Mapping]]]=...) -> None:
...
class PostEvent(_message.Message):
__slots__ = ['type', 'data']
TYPE_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
type: str
data: Post
def __init__(self, type: _Optional[str]=..., data: _Optional[_Union[Post, _Mapping]]=...) -> None:
...

View File

@@ -0,0 +1,113 @@
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
from . import post_pb2 as post__pb2
class PostServiceStub(object):
"""Missing associated documentation comment in .proto file."""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.CreatePost = channel.unary_unary('/panels.post.v1.PostService/CreatePost', request_serializer=post__pb2.CreatePostRequest.SerializeToString, response_deserializer=post__pb2.Post.FromString)
self.GetPost = channel.unary_unary('/panels.post.v1.PostService/GetPost', request_serializer=post__pb2.GetPostRequest.SerializeToString, response_deserializer=post__pb2.Post.FromString)
self.GetPanelPost = channel.unary_unary('/panels.post.v1.PostService/GetPanelPost', request_serializer=post__pb2.GetPanelPostRequest.SerializeToString, response_deserializer=post__pb2.Post.FromString)
self.UpdatePost = channel.unary_unary('/panels.post.v1.PostService/UpdatePost', request_serializer=post__pb2.UpdatePostRequest.SerializeToString, response_deserializer=post__pb2.Post.FromString)
self.DeletePost = channel.unary_unary('/panels.post.v1.PostService/DeletePost', request_serializer=post__pb2.DeletePostRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString)
self.GetFeedPosts = channel.unary_unary('/panels.post.v1.PostService/GetFeedPosts', request_serializer=post__pb2.GetFeedPostsRequest.SerializeToString, response_deserializer=post__pb2.FeedPosts.FromString)
self.GetUserPosts = channel.unary_unary('/panels.post.v1.PostService/GetUserPosts', request_serializer=post__pb2.GetUserPostsRequest.SerializeToString, response_deserializer=post__pb2.UserPosts.FromString)
self.GetPanelPosts = channel.unary_unary('/panels.post.v1.PostService/GetPanelPosts', request_serializer=post__pb2.GetPanelPostsRequest.SerializeToString, response_deserializer=post__pb2.PanelPosts.FromString)
class PostServiceServicer(object):
"""Missing associated documentation comment in .proto file."""
def CreatePost(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetPost(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetPanelPost(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def UpdatePost(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def DeletePost(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetFeedPosts(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetUserPosts(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetPanelPosts(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_PostServiceServicer_to_server(servicer, server):
rpc_method_handlers = {'CreatePost': grpc.unary_unary_rpc_method_handler(servicer.CreatePost, request_deserializer=post__pb2.CreatePostRequest.FromString, response_serializer=post__pb2.Post.SerializeToString), 'GetPost': grpc.unary_unary_rpc_method_handler(servicer.GetPost, request_deserializer=post__pb2.GetPostRequest.FromString, response_serializer=post__pb2.Post.SerializeToString), 'GetPanelPost': grpc.unary_unary_rpc_method_handler(servicer.GetPanelPost, request_deserializer=post__pb2.GetPanelPostRequest.FromString, response_serializer=post__pb2.Post.SerializeToString), 'UpdatePost': grpc.unary_unary_rpc_method_handler(servicer.UpdatePost, request_deserializer=post__pb2.UpdatePostRequest.FromString, response_serializer=post__pb2.Post.SerializeToString), 'DeletePost': grpc.unary_unary_rpc_method_handler(servicer.DeletePost, request_deserializer=post__pb2.DeletePostRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString), 'GetFeedPosts': grpc.unary_unary_rpc_method_handler(servicer.GetFeedPosts, request_deserializer=post__pb2.GetFeedPostsRequest.FromString, response_serializer=post__pb2.FeedPosts.SerializeToString), 'GetUserPosts': grpc.unary_unary_rpc_method_handler(servicer.GetUserPosts, request_deserializer=post__pb2.GetUserPostsRequest.FromString, response_serializer=post__pb2.UserPosts.SerializeToString), 'GetPanelPosts': grpc.unary_unary_rpc_method_handler(servicer.GetPanelPosts, request_deserializer=post__pb2.GetPanelPostsRequest.FromString, response_serializer=post__pb2.PanelPosts.SerializeToString)}
generic_handler = grpc.method_handlers_generic_handler('panels.post.v1.PostService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
class PostService(object):
"""Missing associated documentation comment in .proto file."""
@staticmethod
def CreatePost(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.post.v1.PostService/CreatePost', post__pb2.CreatePostRequest.SerializeToString, post__pb2.Post.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def GetPost(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.post.v1.PostService/GetPost', post__pb2.GetPostRequest.SerializeToString, post__pb2.Post.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def GetPanelPost(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.post.v1.PostService/GetPanelPost', post__pb2.GetPanelPostRequest.SerializeToString, post__pb2.Post.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def UpdatePost(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.post.v1.PostService/UpdatePost', post__pb2.UpdatePostRequest.SerializeToString, post__pb2.Post.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def DeletePost(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.post.v1.PostService/DeletePost', post__pb2.DeletePostRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def GetFeedPosts(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.post.v1.PostService/GetFeedPosts', post__pb2.GetFeedPostsRequest.SerializeToString, post__pb2.FeedPosts.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def GetUserPosts(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.post.v1.PostService/GetUserPosts', post__pb2.GetUserPostsRequest.SerializeToString, post__pb2.UserPosts.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def GetPanelPosts(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.post.v1.PostService/GetPanelPosts', post__pb2.GetPanelPostsRequest.SerializeToString, post__pb2.PanelPosts.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

View File

@@ -0,0 +1,36 @@
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_sym_db = _symbol_database.Default()
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nuser.proto\x12\x0epanels.user.v1\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto"\x96\x01\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x10\n\x08is_admin\x18\x03 \x01(\x08\x12.\n\ncreated_at\x18\x04 \x01(\x0b2\x1a.google.protobuf.Timestamp\x12.\n\nupdated_at\x18\x05 \x01(\x0b2\x1a.google.protobuf.Timestamp"1\n\x0bUserMutable\x12\x15\n\x08username\x18\x01 \x01(\tH\x00\x88\x01\x01B\x0b\n\t_username">\n\x11CreateUserRequest\x12)\n\x04data\x18\x01 \x01(\x0b2\x1b.panels.user.v1.UserMutable" \n\x12GetUserByIdRequest\x12\n\n\x02id\x18\x01 \x01(\t"(\n\x14GetUserByNameRequest\x12\x10\n\x08username\x18\x01 \x01(\t"N\n\x15UpdateUserByIdRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12)\n\x04data\x18\x02 \x01(\x0b2\x1b.panels.user.v1.UserMutable"V\n\x17UpdateUserByNameRequest\x12\x10\n\x08username\x18\x01 \x01(\t\x12)\n\x04data\x18\x02 \x01(\x0b2\x1b.panels.user.v1.UserMutable"#\n\x15DeleteUserByIdRequest\x12\n\n\x02id\x18\x01 \x01(\t"+\n\x17DeleteUserByNameRequest\x12\x10\n\x08username\x18\x01 \x01(\t"=\n\tUserEvent\x12\x0c\n\x04type\x18\x01 \x01(\t\x12"\n\x04data\x18\x02 \x01(\x0b2\x14.panels.user.v1.User2\xb4\x04\n\x0bUserService\x12G\n\nCreateUser\x12!.panels.user.v1.CreateUserRequest\x1a\x14.panels.user.v1.User"\x00\x12E\n\x07GetUser\x12".panels.user.v1.GetUserByIdRequest\x1a\x14.panels.user.v1.User"\x00\x12M\n\rGetUserByName\x12$.panels.user.v1.GetUserByNameRequest\x1a\x14.panels.user.v1.User"\x00\x12K\n\nUpdateUser\x12%.panels.user.v1.UpdateUserByIdRequest\x1a\x14.panels.user.v1.User"\x00\x12S\n\x10UpdateUserByName\x12\'.panels.user.v1.UpdateUserByNameRequest\x1a\x14.panels.user.v1.User"\x00\x12M\n\nDeleteUser\x12%.panels.user.v1.DeleteUserByIdRequest\x1a\x16.google.protobuf.Empty"\x00\x12U\n\x10DeleteUserByName\x12\'.panels.user.v1.DeleteUserByNameRequest\x1a\x16.google.protobuf.Empty"\x00b\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'user_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_globals['_USER']._serialized_start = 93
_globals['_USER']._serialized_end = 243
_globals['_USERMUTABLE']._serialized_start = 245
_globals['_USERMUTABLE']._serialized_end = 294
_globals['_CREATEUSERREQUEST']._serialized_start = 296
_globals['_CREATEUSERREQUEST']._serialized_end = 358
_globals['_GETUSERBYIDREQUEST']._serialized_start = 360
_globals['_GETUSERBYIDREQUEST']._serialized_end = 392
_globals['_GETUSERBYNAMEREQUEST']._serialized_start = 394
_globals['_GETUSERBYNAMEREQUEST']._serialized_end = 434
_globals['_UPDATEUSERBYIDREQUEST']._serialized_start = 436
_globals['_UPDATEUSERBYIDREQUEST']._serialized_end = 514
_globals['_UPDATEUSERBYNAMEREQUEST']._serialized_start = 516
_globals['_UPDATEUSERBYNAMEREQUEST']._serialized_end = 602
_globals['_DELETEUSERBYIDREQUEST']._serialized_start = 604
_globals['_DELETEUSERBYIDREQUEST']._serialized_end = 639
_globals['_DELETEUSERBYNAMEREQUEST']._serialized_start = 641
_globals['_DELETEUSERBYNAMEREQUEST']._serialized_end = 684
_globals['_USEREVENT']._serialized_start = 686
_globals['_USEREVENT']._serialized_end = 747
_globals['_USERSERVICE']._serialized_start = 750
_globals['_USERSERVICE']._serialized_end = 1314

View File

@@ -0,0 +1,100 @@
from google.protobuf import empty_pb2 as _empty_pb2
from google.protobuf import timestamp_pb2 as _timestamp_pb2
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class User(_message.Message):
__slots__ = ['id', 'username', 'is_admin', 'created_at', 'updated_at']
ID_FIELD_NUMBER: _ClassVar[int]
USERNAME_FIELD_NUMBER: _ClassVar[int]
IS_ADMIN_FIELD_NUMBER: _ClassVar[int]
CREATED_AT_FIELD_NUMBER: _ClassVar[int]
UPDATED_AT_FIELD_NUMBER: _ClassVar[int]
id: str
username: str
is_admin: bool
created_at: _timestamp_pb2.Timestamp
updated_at: _timestamp_pb2.Timestamp
def __init__(self, id: _Optional[str]=..., username: _Optional[str]=..., is_admin: bool=..., created_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]]=..., updated_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]]=...) -> None:
...
class UserMutable(_message.Message):
__slots__ = ['username']
USERNAME_FIELD_NUMBER: _ClassVar[int]
username: str
def __init__(self, username: _Optional[str]=...) -> None:
...
class CreateUserRequest(_message.Message):
__slots__ = ['data']
DATA_FIELD_NUMBER: _ClassVar[int]
data: UserMutable
def __init__(self, data: _Optional[_Union[UserMutable, _Mapping]]=...) -> None:
...
class GetUserByIdRequest(_message.Message):
__slots__ = ['id']
ID_FIELD_NUMBER: _ClassVar[int]
id: str
def __init__(self, id: _Optional[str]=...) -> None:
...
class GetUserByNameRequest(_message.Message):
__slots__ = ['username']
USERNAME_FIELD_NUMBER: _ClassVar[int]
username: str
def __init__(self, username: _Optional[str]=...) -> None:
...
class UpdateUserByIdRequest(_message.Message):
__slots__ = ['id', 'data']
ID_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
id: str
data: UserMutable
def __init__(self, id: _Optional[str]=..., data: _Optional[_Union[UserMutable, _Mapping]]=...) -> None:
...
class UpdateUserByNameRequest(_message.Message):
__slots__ = ['username', 'data']
USERNAME_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
username: str
data: UserMutable
def __init__(self, username: _Optional[str]=..., data: _Optional[_Union[UserMutable, _Mapping]]=...) -> None:
...
class DeleteUserByIdRequest(_message.Message):
__slots__ = ['id']
ID_FIELD_NUMBER: _ClassVar[int]
id: str
def __init__(self, id: _Optional[str]=...) -> None:
...
class DeleteUserByNameRequest(_message.Message):
__slots__ = ['username']
USERNAME_FIELD_NUMBER: _ClassVar[int]
username: str
def __init__(self, username: _Optional[str]=...) -> None:
...
class UserEvent(_message.Message):
__slots__ = ['type', 'data']
TYPE_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
type: str
data: User
def __init__(self, type: _Optional[str]=..., data: _Optional[_Union[User, _Mapping]]=...) -> None:
...

View File

@@ -0,0 +1,102 @@
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
from . import user_pb2 as user__pb2
class UserServiceStub(object):
"""Missing associated documentation comment in .proto file."""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.CreateUser = channel.unary_unary('/panels.user.v1.UserService/CreateUser', request_serializer=user__pb2.CreateUserRequest.SerializeToString, response_deserializer=user__pb2.User.FromString)
self.GetUser = channel.unary_unary('/panels.user.v1.UserService/GetUser', request_serializer=user__pb2.GetUserByIdRequest.SerializeToString, response_deserializer=user__pb2.User.FromString)
self.GetUserByName = channel.unary_unary('/panels.user.v1.UserService/GetUserByName', request_serializer=user__pb2.GetUserByNameRequest.SerializeToString, response_deserializer=user__pb2.User.FromString)
self.UpdateUser = channel.unary_unary('/panels.user.v1.UserService/UpdateUser', request_serializer=user__pb2.UpdateUserByIdRequest.SerializeToString, response_deserializer=user__pb2.User.FromString)
self.UpdateUserByName = channel.unary_unary('/panels.user.v1.UserService/UpdateUserByName', request_serializer=user__pb2.UpdateUserByNameRequest.SerializeToString, response_deserializer=user__pb2.User.FromString)
self.DeleteUser = channel.unary_unary('/panels.user.v1.UserService/DeleteUser', request_serializer=user__pb2.DeleteUserByIdRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString)
self.DeleteUserByName = channel.unary_unary('/panels.user.v1.UserService/DeleteUserByName', request_serializer=user__pb2.DeleteUserByNameRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString)
class UserServiceServicer(object):
"""Missing associated documentation comment in .proto file."""
def CreateUser(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetUser(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def GetUserByName(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def UpdateUser(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def UpdateUserByName(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def DeleteUser(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def DeleteUserByName(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_UserServiceServicer_to_server(servicer, server):
rpc_method_handlers = {'CreateUser': grpc.unary_unary_rpc_method_handler(servicer.CreateUser, request_deserializer=user__pb2.CreateUserRequest.FromString, response_serializer=user__pb2.User.SerializeToString), 'GetUser': grpc.unary_unary_rpc_method_handler(servicer.GetUser, request_deserializer=user__pb2.GetUserByIdRequest.FromString, response_serializer=user__pb2.User.SerializeToString), 'GetUserByName': grpc.unary_unary_rpc_method_handler(servicer.GetUserByName, request_deserializer=user__pb2.GetUserByNameRequest.FromString, response_serializer=user__pb2.User.SerializeToString), 'UpdateUser': grpc.unary_unary_rpc_method_handler(servicer.UpdateUser, request_deserializer=user__pb2.UpdateUserByIdRequest.FromString, response_serializer=user__pb2.User.SerializeToString), 'UpdateUserByName': grpc.unary_unary_rpc_method_handler(servicer.UpdateUserByName, request_deserializer=user__pb2.UpdateUserByNameRequest.FromString, response_serializer=user__pb2.User.SerializeToString), 'DeleteUser': grpc.unary_unary_rpc_method_handler(servicer.DeleteUser, request_deserializer=user__pb2.DeleteUserByIdRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString), 'DeleteUserByName': grpc.unary_unary_rpc_method_handler(servicer.DeleteUserByName, request_deserializer=user__pb2.DeleteUserByNameRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString)}
generic_handler = grpc.method_handlers_generic_handler('panels.user.v1.UserService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
class UserService(object):
"""Missing associated documentation comment in .proto file."""
@staticmethod
def CreateUser(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.user.v1.UserService/CreateUser', user__pb2.CreateUserRequest.SerializeToString, user__pb2.User.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def GetUser(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.user.v1.UserService/GetUser', user__pb2.GetUserByIdRequest.SerializeToString, user__pb2.User.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def GetUserByName(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.user.v1.UserService/GetUserByName', user__pb2.GetUserByNameRequest.SerializeToString, user__pb2.User.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def UpdateUser(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.user.v1.UserService/UpdateUser', user__pb2.UpdateUserByIdRequest.SerializeToString, user__pb2.User.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def UpdateUserByName(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.user.v1.UserService/UpdateUserByName', user__pb2.UpdateUserByNameRequest.SerializeToString, user__pb2.User.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def DeleteUser(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.user.v1.UserService/DeleteUser', user__pb2.DeleteUserByIdRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def DeleteUserByName(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None):
return grpc.experimental.unary_unary(request, target, '/panels.user.v1.UserService/DeleteUserByName', user__pb2.DeleteUserByNameRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

View File

@@ -0,0 +1,97 @@
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel
from google.protobuf import timestamp_pb2
from comment_service.models.proto import comment_pb2
from comment_service.models.exceptions import ServiceException, ServiceErrorCode
# Validators
def is_valid_comment_msg(message: str) -> bool:
if len(message) < 3 or len(message) > 512:
return False
return True
# Service Models
class Comment(BaseModel):
id: int
post_id: str
author_id: str
message: str
created_at: datetime
updated_at: Optional[datetime] = None
@classmethod
def to_protobuf(cls, comment: "Comment") -> comment_pb2.Comment:
created_at = timestamp_pb2.Timestamp()
created_at.FromDatetime(comment.created_at)
updated_at = None
if comment.updated_at is not None:
updated_at = timestamp_pb2.Timestamp()
updated_at.FromDatetime(comment.updated_at)
return comment_pb2.Comment(
id=str(comment.id),
post_id=comment.post_id,
author_id=comment.author_id,
message=comment.message,
created_at=created_at,
updated_at=updated_at,
)
class CommentCreate(BaseModel):
post_id: str
author_id: str
message: str # todo: validation on message (wrap validator for is_valid_comment_msg)
@classmethod
def from_protobuf(cls, request: comment_pb2.CreateCommentRequest) -> "CommentCreate":
return cls(
post_id=request.post_id,
author_id=request.author_id,
message=request.data.message,
)
class CommentUpdate(BaseModel):
message: Optional[str] = None # todo: validation on message (if set use validator is_valid_comment_msg)
@classmethod
def from_protobuf(cls, request: comment_pb2.UpdateCommentRequest) -> "CommentUpdate":
return cls(
message=request.data.message
)
# Repository Interfaces
class CommentRepository:
async def get_comment(self, comment_id: int) -> Comment:
raise ServiceException("unimplemented internal repository method", ServiceErrorCode.SERVICE_ERROR)
async def get_post_comments(self, post_id: str) -> List[Comment]:
raise ServiceException("unimplemented internal repository method", ServiceErrorCode.SERVICE_ERROR)
async def create_comment(self, data: CommentCreate) -> Comment:
raise ServiceException("unimplemented internal repository method", ServiceErrorCode.SERVICE_ERROR)
async def update_comment(self, comment_id: int, data: CommentUpdate) -> Comment:
raise ServiceException("unimplemented internal repository method", ServiceErrorCode.SERVICE_ERROR)
async def delete_comment(self, comment_id: int) -> None:
raise ServiceException("unimplemented internal repository method", ServiceErrorCode.SERVICE_ERROR)
class CommentDBRepository(CommentRepository):
async def delete_post_comments(self, post_id: str) -> None:
raise ServiceException("unimplemented internal repository method", ServiceErrorCode.SERVICE_ERROR)
async def delete_user_comments(self, user_id: str) -> None:
raise ServiceException("unimplemented internal repository method", ServiceErrorCode.SERVICE_ERROR)

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS comments CASCADE;

View File

@@ -0,0 +1,11 @@
CREATE TABLE comments (
"id" serial PRIMARY KEY,
"post_id" varchar(64) NOT NULL,
"author_id" varchar(64) NOT NULL,
"message" varchar(512) NOT NULL,
"created_at" timestamp NOT NULL DEFAULT timezone('utc', now()),
"updated_at" timestamp
);

View File

@@ -0,0 +1,94 @@
from typing import List
from databases import Database
from comment_service.events.producer import CommentEventProducer
from comment_service.models.exceptions import ServiceException, ServiceErrorCode
from comment_service.models.service import CommentDBRepository, Comment, CommentCreate, CommentUpdate
class ServiceDBRepository(CommentDBRepository):
"""Database repository responsible for actions
relating to the postgres database.
This repository will be utilised by other upstream repositories
or the Kafka event consumers.
Attributes:
_db (Database): The postgres database connection handler.
_event_prod (CommentEventProducer): Used to dispatch events upon execution of a CRUD action.
"""
def __init__(self, db: Database, event_producer: CommentEventProducer) -> None:
self._db = db
self._event_prod = event_producer
def _result_to_comment(self, result) -> Comment:
return Comment(
id=result.id,
post_id=result.post_id,
author_id=result.author_id,
message=result.message,
created_at=result.created_at,
updated_at=result._mapping.get("updated_at", None)
)
async def get_post_comments(self, post_id: str) -> List[Comment]:
query = "SELECT id, post_id, author_id, message, created_at, updated_at FROM comments WHERE post_id = :post_id"
rows = await self._db.fetch_all(query=query, values={"post_id": post_id})
return [self._result_to_comment(result) for result in rows]
async def create_comment(self, data: CommentCreate) -> Comment:
query = "INSERT INTO comments (post_id, author_id, message) VALUES (:post_id, :author_id, :message) RETURNING id"
result = await self._db.execute(query=query, values={"post_id": data.post_id, "author_id": data.author_id, "message": data.message})
comment = await self.get_comment(result)
await self._event_prod.send_created_event(comment)
return comment
async def update_comment(self, comment_id: int, data: CommentUpdate) -> Comment:
query = "UPDATE comments SET message = :message, updated_at = now() WHERE id = :comment_id"
await self._db.execute(query=query, values={"message": data.message, "comment_id": comment_id})
comment = await self.get_comment(comment_id)
await self._event_prod.send_updated_event(comment)
return comment
async def delete_comment(self, comment_id: int) -> None:
comment = await self.get_comment(comment_id)
query = "DELETE FROM comments WHERE id = :comment_id"
await self._db.execute(query=query, values={"comment_id": comment_id})
await self._event_prod.send_deleted_event(comment)
async def get_comment(self, comment_id: int) -> Comment:
query = "SELECT id, post_id, author_id, message, created_at, updated_at FROM comments WHERE id = :comment_id"
result = await self._db.fetch_one(query=query, values={"comment_id": comment_id})
if result is None:
raise ServiceException(message="no comment found", error_code=ServiceErrorCode.NOT_FOUND)
return self._result_to_comment(result)
async def delete_post_comments(self, post_id: str) -> None:
comments = await self.get_post_comments(post_id)
query = "DELETE FROM comments WHERE post_id = :post_id"
await self._db.execute(query=query, values={"post_id": post_id})
for comment in comments:
await self._event_prod.send_deleted_event(comment)
async def delete_user_comments(self, user_id: str) -> None:
comments = await self.get_post_comments(user_id)
query = "DELETE FROM comments WHERE author_id = :author_id"
await self._db.execute(query=query, values={"author_id": user_id})
for comment in comments:
await self._event_prod.send_deleted_event(comment)
async def _get_user_comments(self, user_id: str) -> List[Comment]:
query = "SELECT id, post_id, author_id, message, created_at, updated_at FROM comments WHERE author_id = :user_id"
rows = await self._db.fetch_all(query=query, values={"user_id": user_id})
return [self._result_to_comment(result) for result in rows]

View File

@@ -0,0 +1,41 @@
import logging
from databases import Database
from comment_service.models.config import Config
from comment_service.events.producer import CommentEventProducer
from comment_service.postgres.repository import ServiceDBRepository
async def connect_database(config: Config) -> Database:
"""Opens a connection to the database.
Args:
config (Config): The app configuration.
Returns:
A connected databases.Database instance
"""
db = Database(config.postgres_dsn)
try:
await db.connect()
except Exception:
logging.error("failed to connect to postgresql database")
raise
return db
async def create_db_repository(config: Config, event_producer: CommentEventProducer) -> ServiceDBRepository:
"""Create the database repository.
Open a database connection and instantialise the
database repository.
Returns:
ServiceDBRepository
"""
db = await connect_database(config)
return ServiceDBRepository(db, event_producer)

View File

@@ -0,0 +1,76 @@
import pickle
import logging
from typing import Type, List
import redis.asyncio as redis
from comment_service.models.service import CommentRepository, Comment, CommentCreate, CommentUpdate
class ServiceRedisRepository(CommentRepository):
"""The Redis repository is responsible for caching
requests to help reduce the amount of database calls,
allowing for faster data access.
If the Redis repository does not have the data cached,
or does not cache data for that request, then the call is
passed downstream to the database repository.
Attributes:
_conn (redis.asyncio.redis.Redis): The Redis connection.
_repo (CommentRepository): The next downstream repository (the DB repo).
"""
def __init__(self, redis_conn: redis.Redis, downstream_repo: Type[CommentRepository]) -> None:
self._conn = redis_conn
self._repo = downstream_repo
async def _purge_cached_comments(self, post_id: str) -> None:
try:
self._conn.delete(post_id)
except Exception:
pass
async def get_comment(self, comment_id: int) -> Comment:
return await self._repo.get_comment(comment_id)
async def get_post_comments(self, post_id: str) -> List[Comment]:
post_id = post_id.lower()
failure = False
try:
# check for a cached version of the post comments
response = await self._conn.get(post_id)
if response is not None:
comments = pickle.loads(response)
assert type(comments) == list
return comments
except Exception as e:
failure = True
logging.error("Redis Repo: error whilst getting post comments", e)
pass
comments = await self._repo.get_post_comments(post_id)
if failure is False:
# cache the retrieved comments
try:
await self._conn.set(post_id, pickle.dumps(comments), ex=120) # TTL: 2 minutes
except Exception:
logging.error("Redis Repo: error whilst caching post comments", e)
pass
return comments
async def create_comment(self, data: CommentCreate) -> Comment:
comment = await self._repo.create_comment(data)
await self._purge_cached_comments(comment.post_id)
return comment
async def update_comment(self, comment_id: int, data: CommentUpdate) -> Comment:
comment = await self._repo.update_comment(comment_id, data)
await self._purge_cached_comments(comment.post_id)
return comment
async def delete_comment(self, comment_id: int) -> None:
# todo: purge cache of comments on post (instead of waiting for TTL expiry)
await self._repo.delete_comment(comment_id)

View File

@@ -0,0 +1,48 @@
from typing import Type
import redis.asyncio as redis
from comment_service.models.config import Config
from comment_service.models.service import CommentRepository
from comment_service.redis.repository import ServiceRedisRepository
async def connect_redis(config: Config) -> redis.Redis:
"""Opens a connection to Redis.
Args:
config (Config): The app configuration.
Returns:
A connected redis.Redis instance
"""
host, port = config.redis_host, 5432
try:
host, port = config.redis_host.split(":")
port = int(port)
except Exception:
pass
conn = redis.Redis(host=host, port=port, password=config.redis_pass)
await conn.ping()
return conn
async def create_redis_repository(config: Config, downstream_repo: Type[CommentRepository]) -> ServiceRedisRepository:
"""Create the Redis repository.
Open a Redis connection and instantialise the
Redis repository.
Args:
downstream_repo (Type[CommentRepository]): The next downstream repository
that will be called by the Redis repository. (in this case the database
repository)
Returns:
ServiceRedisRepository
"""
redis_conn = await connect_redis(config)
return ServiceRedisRepository(redis_conn=redis_conn, downstream_repo=downstream_repo)

View File

@@ -0,0 +1,289 @@
import logging
import traceback
from typing import Type
from google.protobuf import empty_pb2
from grpc import RpcContext, StatusCode
from comment_service.models.exceptions import ServiceException
from comment_service.models.service import CommentRepository, Comment, CommentCreate, CommentUpdate
from comment_service.models.proto import comment_pb2, comment_pb2_grpc
class CommentServicer(comment_pb2_grpc.CommentServiceServicer):
"""Contains definitions for the service's RPC methods.
Requests are converted from protobuf to business model
form, then directed to the service repository, where the
response is then translated back to protobuf.
Attributes:
_svc_repo (Type[CommentRepository]): The highest level service repository.
"""
def __init__(self, svc_repo: Type[CommentRepository]) -> None:
self._svc_repo = svc_repo
def _apply_error(self, context: RpcContext, code: StatusCode, msg: str) -> None:
"""Apply an error to a given RPC context.
Args:
context (grpc.RpcContext): The context to apply the error to.
code (grpc.StatusCode): The gRPC status code.
msg (str): The error details.
"""
context.set_code(code)
context.set_details(msg)
def _apply_unknown_error(self, context: RpcContext) -> None:
"""Apply a de facto error fallback message.
Args:
context (grpc.RpcContext): The context to apply the error to.
"""
self._apply_error(context, StatusCode.UNKNOWN, "unknown error occured")
async def CreateComment(self, request: comment_pb2.CreateCommentRequest, context: RpcContext) -> comment_pb2.Comment:
"""CreateComment RPC Call
Args:
request (comment_pb2.CreateCommentRequest): The request parameters.
context (grpc.RpcContext): The context of the RPC call.
Returns:
comment_pb2.Comment: With a succesful comment creation.
"""
# vaLidate the request inputs
if request.post_id == "":
self._apply_error(
context,
code=StatusCode.INVALID_ARGUMENT,
msg="post not provided"
)
return
if request.author_id == "":
self._apply_error(
context,
code=StatusCode.INVALID_ARGUMENT,
msg="author not provided"
)
return
if request.data == None:
self._apply_error(
context,
code=StatusCode.INVALID_ARGUMENT,
msg="malformed request"
)
return
if request.data.message == "":
self._apply_error(
context,
code=StatusCode.INVALID_ARGUMENT,
msg="comment message not provided"
)
return
# convert to service model from protobuf
try:
data = CommentCreate.from_protobuf(request)
comment = await self._svc_repo.create_comment(data)
except ServiceException as err:
err.apply_to_rpc(context)
return
except Exception:
logging.error(traceback.format_exc())
self._apply_unknown_error(context)
return
# convert comment to protobuf form
return Comment.to_protobuf(comment)
async def UpdateComment(self, request: comment_pb2.UpdateCommentRequest, context: RpcContext) -> comment_pb2.Comment:
"""UpdateComment RPC Call
Args:
request (comment_pb2.UpdateCommentRequest): The request parameters.
context (grpc.RpcContext): The context of the RPC call.
Returns:
comment_pb2.Comment: The updated comment details (if succesfully updated).
"""
# vaLidate the request inputs
if request.id == "":
self._apply_error(
context,
code=StatusCode.INVALID_ARGUMENT,
msg="comment not provided"
)
return
elif not request.id.isnumeric():
self._apply_error(
context,
code=StatusCode.INVALID_ARGUMENT,
msg="invalid comment id provided"
)
return
if request.data == None:
self._apply_error(
context,
code=StatusCode.INVALID_ARGUMENT,
msg="malformed request"
)
return
if request.data.message == "":
self._apply_error(
context,
code=StatusCode.INVALID_ARGUMENT,
msg="comment message not provided"
)
return
# convert to service model from protobuf
try:
comment_id = int(request.id)
data = CommentUpdate.from_protobuf(request)
comment = await self._svc_repo.update_comment(comment_id, data)
except ServiceException as err:
err.apply_to_rpc(context)
return
except Exception:
logging.error(traceback.format_exc())
self._apply_unknown_error(context)
return
# convert comment to protobuf form
return Comment.to_protobuf(comment)
async def DeleteComment(self, request: comment_pb2.DeleteCommentRequest, context: RpcContext) -> empty_pb2.Empty:
"""DeleteComment RPC Call
Args:
request (comment_pb2.DeleteCommentRequest): The request parameters.
context (grpc.RpcContext): The context of the RPC call.
Returns:
empty_pb2.Empty: Empty protobuf response (in effect returns None).
"""
# vaLidate the request inputs
if request.id == "":
self._apply_error(
context,
code=StatusCode.INVALID_ARGUMENT,
msg="comment not provided"
)
return
if not request.id.isnumeric():
self._apply_error(
context,
code=StatusCode.INVALID_ARGUMENT,
msg="invalid comment id provided"
)
return
# attempt to delete the comment
try:
comment_id = int(request.id)
await self._svc_repo.delete_comment(comment_id)
except ServiceException as err:
err.apply_to_rpc(context)
return
except Exception:
logging.error(traceback.format_exc())
self._apply_unknown_error(context)
return
return empty_pb2.Empty()
async def GetComment(self, request: comment_pb2.GetCommentRequest, context: RpcContext) -> comment_pb2.PostComments:
"""GetComment RPC Call
Returns a comment by comment id.
Args:
request (comment_pb2.GetCommentRequest): The request parameters.
context (grpc.RpcContext): The context of the RPC call.
Returns:
comment_pb2.Comment: The located comment
"""
# vaLidate the request inputs
if request.id == "":
self._apply_error(
context,
code=StatusCode.INVALID_ARGUMENT,
msg="comment id not provided"
)
return
if not request.id.isnumeric():
self._apply_error(
context,
code=StatusCode.INVALID_ARGUMENT,
msg="invalid comment id provided"
)
return
# attempt to get the comment
try:
comment = await self._svc_repo.get_comment(int(request.id))
except ServiceException as err:
err.apply_to_rpc(context)
return
except Exception:
logging.error(traceback.format_exc())
self._apply_unknown_error(context)
return
return Comment.to_protobuf(comment)
async def GetPostComments(self, request: comment_pb2.GetPostCommentsRequest, context: RpcContext) -> comment_pb2.PostComments:
"""GetPostComments RPC Call
Returns a list of comments that a post has.
TODO:
Implement pagination (?after=comment_id or some effect)
to return more comments from a post.
Args:
request (comment_pb2.UpdateCommentRequest): The request parameters.
context (grpc.RpcContext): The context of the RPC call.
Returns:
comment_pb2.PostComments: containing a list of the post's comments
"""
# vaLidate the request inputs
if request.post_id == "":
self._apply_error(
context,
code=StatusCode.INVALID_ARGUMENT,
msg="post id not provided"
)
return
# attempt to get the comments
try:
comments = await self._svc_repo.get_post_comments(request.post_id)
except ServiceException as err:
err.apply_to_rpc(context)
return
except Exception:
logging.error(traceback.format_exc())
self._apply_unknown_error(context)
return
# convert to protobuf
return comment_pb2.PostComments(comments=[Comment.to_protobuf(comment) for comment in comments])

View File

@@ -0,0 +1,52 @@
import logging
from typing import Type
import grpc
from grpc_health.v1 import health, health_pb2_grpc
from comment_service.rpc.comment import CommentServicer
from comment_service.models.proto import comment_pb2_grpc
from comment_service.models.service import CommentRepository
class RPCServerWrapper:
"""A wrapper class for the RPC server.
Attributes:
_grpc_server (grpc.aio.Server): The gRPC server instance.
"""
def __init__(self, svc_repo: Type[CommentRepository]) -> None:
"""Creates the gRPC server and adds the servicers.
Args:
svc_repo (Type[CommentRepository]): The service repository to pass to the servicers.
"""
self._grpc_server = grpc.aio.server()
self._grpc_server.add_insecure_port("[::]:9090")
comment_servicer = CommentServicer(svc_repo)
comment_pb2_grpc.add_CommentServiceServicer_to_server(comment_servicer, self._grpc_server)
health_servicer = health.aio.HealthServicer()
health_pb2_grpc.add_HealthServicer_to_server(health_servicer, self._grpc_server)
async def start(self) -> None:
"""Begin serving RPC asynchronously."""
logging.info("attempting to serve RPC...")
await self._grpc_server.start()
await self._grpc_server.wait_for_termination()
def create_rpc_server(svc_repo: Type[CommentRepository]) -> RPCServerWrapper:
"""Instantialise the RPC server wrapper.
Args:
svc_repo (Type[CommentRepository]): The service repository for the RPC servicers to interface with.
Returns:
RPCServerWrapper
"""
return RPCServerWrapper(svc_repo)

View File

@@ -0,0 +1,30 @@
from typing import Type, List
from comment_service.models.service import CommentRepository, Comment, CommentCreate, CommentUpdate
class ServiceRepository(CommentRepository):
"""The comment service repository.
Attributes:
_repo (Type[CommentRepository]): The downstream repository (Redis Repository -> DB Repository).
"""
def __init__(self, downstream_repo: Type[CommentRepository]) -> None:
self._repo = downstream_repo
async def get_comment(self, comment_id: int) -> Comment:
return await self._repo.get_comment(comment_id)
async def get_post_comments(self, post_id: str) -> List[Comment]:
# todo: pagination
return await self._repo.get_post_comments(post_id)
async def create_comment(self, data: CommentCreate) -> Comment:
return await self._repo.create_comment(data)
async def update_comment(self, comment_id: int, data: CommentUpdate) -> Comment:
return await self._repo.update_comment(comment_id, data)
async def delete_comment(self, comment_id: int) -> None:
await self._repo.delete_comment(comment_id)