diff --git a/.env.example b/.env.example index 51d0f4a2..d10b482d 100644 --- a/.env.example +++ b/.env.example @@ -52,6 +52,9 @@ MICROSERVICES_NOTIFICATION_INTERNAL_PORT=50055 MICROSERVICES_THE_MONKEYS_AI_ENGINE=the_monkeys_ai_engine MICROSERVICES_AI_ENGINE_PORT=50057 MICROSERVICES_AI_ENGINE_INTERNAL_PORT=50057 +MICROSERVICE_REPORTS_SERVICE=the_monkeys_reports +MICROSERVICE_REPORTS_SERVICE_PORT=50058 +MICROSERVICE_REPORTS_SERVICE_INTERNAL_PORT=50058 MICROSERVICES_AI_ENGINE_HEALTH_PORT=51600 MICROSERVICES_AI_ENGINE_HEALTH_INTERNAL_PORT=51600 MICROSERVICES_THE_MONKEYS_ACTIVITY=the_monkeys_activity diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index f51f94a4..abf7e351 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -62,7 +62,9 @@ jobs: build_args: "" - name: activity dockerfile: microservices/the_monkeys_activity/Dockerfile - context: . + - name: reports + dockerfile: ./microservices/the_monkeys_reports/Dockerfile + context: ./microservices/the_monkeys_reports build_args: "" steps: @@ -295,4 +297,4 @@ jobs: echo "🚀 To deploy:" echo "1. Use your existing docker-compose.yml for infrastructure services" echo "2. Update microservice image references to use registry images above" - echo "3. Or use docker-compose.registry.yml alongside your infrastructure setup" \ No newline at end of file + echo "3. Or use docker-compose.registry.yml alongside your infrastructure setup" diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..009ae1c3 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "monkeys_protos"] + path = monkeys_protos + url = https://github.com/the-monkeys/monkeys_protos.git diff --git a/Makefile b/Makefile index bca72c8d..eb4f4054 100644 --- a/Makefile +++ b/Makefile @@ -45,4 +45,37 @@ proto-gen: freeze: - pip freeze > requirements.txt \ No newline at end of file + pip freeze > requirements.txt + + +include .env + +APP_ENV ?= development + +ifeq ($(APP_ENV), production) + COMPOSE_FILE_EXTENSION := .prod.yml + CONTAINER_RUNTIME := docker +else + COMPOSE_FILE_EXTENSION := .yml +endif + +ifneq (, $(shell which podman)) + CONTAINER_RUNTIME := podman +endif + +ifneq (, $(shell which docker)) + CONTAINER_RUNTIME := docker +endif + +# List of compose files of our services +REPORTS_COMPOSE := microservices/the_monkeys_reports/docker-compose$(COMPOSE_FILE_EXTENSION) +ROOT_COMPOSE := docker-compose$(COMPOSE_FILE_EXTENSION) + +containers/up: + $(CONTAINER_RUNTIME) compose -f $(ROOT_COMPOSE) -f $(REPORTS_COMPOSE) --env-file .env up + +containers/down: + $(CONTAINER_RUNTIME) compose -f $(ROOT_COMPOSE) -f $(REPORTS_COMPOSE) down + +containers/logs: + $(CONTAINER_RUNTIME) compose -f $(ROOT_COMPOSE) -f $(REPORTS_COMPOSE) logs -f diff --git a/config/config.go b/config/config.go index 5c68cc2d..b8a5020f 100644 --- a/config/config.go +++ b/config/config.go @@ -40,6 +40,10 @@ type Microservices struct { TheMonkeysActivity string `mapstructure:"the_monkeys_activity"` ActivityPort int `mapstructure:"activity_port"` ActivityInternalPort int `mapstructure:"activity_internal_port"` + + ReportsService string `mapstructure:"reports_service"` + ReportsServicePort int `mapstructure:"reports_service_port"` + ReportsServiceInternalPort int `mapstructure:"reports_service_internal_port"` } type Database struct { @@ -256,6 +260,9 @@ func bindEnvVars() { viper.BindEnv("microservices.the_monkeys_activity", "MICROSERVICES_THE_MONKEYS_ACTIVITY") viper.BindEnv("microservices.activity_port", "MICROSERVICES_ACTIVITY_PORT") viper.BindEnv("microservices.activity_internal_port", "MICROSERVICES_ACTIVITY_INTERNAL_PORT") + viper.BindEnv("microservices.reports_service", "MICROSERVICE_REPORTS_SERVICE") + viper.BindEnv("microservices.reports_service_port", "MICROSERVICE_REPORTS_SERVICE_PORT") + viper.BindEnv("microservices.reports_service_internal_port", "MICROSERVICE_REPORTS_SERVICE_INTERNAL_PORT") // PostgreSQL viper.BindEnv("postgresql.primary_db.db_username", "POSTGRESQL_PRIMARY_DB_DB_USERNAME") diff --git a/generated/go/gw_report.pb.go b/generated/go/gw_report.pb.go new file mode 100644 index 00000000..7688e887 --- /dev/null +++ b/generated/go/gw_report.pb.go @@ -0,0 +1,497 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.9 +// protoc v6.32.0 +// source: monkeys_protos/gw_report.proto + +package _go + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Report struct { + state protoimpl.MessageState `protogen:"open.v1"` + ReportId string `protobuf:"bytes,1,opt,name=report_id,json=reportId,proto3" json:"report_id,omitempty"` + ReasonType string `protobuf:"bytes,2,opt,name=reason_type,json=reasonType,proto3" json:"reason_type,omitempty"` + FlagCount uint32 `protobuf:"varint,3,opt,name=flag_count,json=flagCount,proto3" json:"flag_count,omitempty"` + ReporterId string `protobuf:"bytes,4,opt,name=reporter_id,json=reporterId,proto3" json:"reporter_id,omitempty"` + ReportedType string `protobuf:"bytes,5,opt,name=reported_type,json=reportedType,proto3" json:"reported_type,omitempty"` + ReportedId string `protobuf:"bytes,6,opt,name=reported_id,json=reportedId,proto3" json:"reported_id,omitempty"` + Status string `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` + ReporterNotes string `protobuf:"bytes,8,opt,name=reporter_notes,json=reporterNotes,proto3" json:"reporter_notes,omitempty"` + ModeratorId string `protobuf:"bytes,9,opt,name=moderator_id,json=moderatorId,proto3" json:"moderator_id,omitempty"` + ModeratorNotes string `protobuf:"bytes,10,opt,name=moderator_notes,json=moderatorNotes,proto3" json:"moderator_notes,omitempty"` + Verdict string `protobuf:"bytes,11,opt,name=verdict,proto3" json:"verdict,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Report) Reset() { + *x = Report{} + mi := &file_monkeys_protos_gw_report_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Report) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Report) ProtoMessage() {} + +func (x *Report) ProtoReflect() protoreflect.Message { + mi := &file_monkeys_protos_gw_report_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Report.ProtoReflect.Descriptor instead. +func (*Report) Descriptor() ([]byte, []int) { + return file_monkeys_protos_gw_report_proto_rawDescGZIP(), []int{0} +} + +func (x *Report) GetReportId() string { + if x != nil { + return x.ReportId + } + return "" +} + +func (x *Report) GetReasonType() string { + if x != nil { + return x.ReasonType + } + return "" +} + +func (x *Report) GetFlagCount() uint32 { + if x != nil { + return x.FlagCount + } + return 0 +} + +func (x *Report) GetReporterId() string { + if x != nil { + return x.ReporterId + } + return "" +} + +func (x *Report) GetReportedType() string { + if x != nil { + return x.ReportedType + } + return "" +} + +func (x *Report) GetReportedId() string { + if x != nil { + return x.ReportedId + } + return "" +} + +func (x *Report) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *Report) GetReporterNotes() string { + if x != nil { + return x.ReporterNotes + } + return "" +} + +func (x *Report) GetModeratorId() string { + if x != nil { + return x.ModeratorId + } + return "" +} + +func (x *Report) GetModeratorNotes() string { + if x != nil { + return x.ModeratorNotes + } + return "" +} + +func (x *Report) GetVerdict() string { + if x != nil { + return x.Verdict + } + return "" +} + +type CreateReportRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ReasonType string `protobuf:"bytes,1,opt,name=reason_type,json=reasonType,proto3" json:"reason_type,omitempty"` + ReporterId string `protobuf:"bytes,2,opt,name=reporter_id,json=reporterId,proto3" json:"reporter_id,omitempty"` + ReportedType string `protobuf:"bytes,3,opt,name=reported_type,json=reportedType,proto3" json:"reported_type,omitempty"` + ReportedId string `protobuf:"bytes,4,opt,name=reported_id,json=reportedId,proto3" json:"reported_id,omitempty"` + ReporterNotes string `protobuf:"bytes,5,opt,name=reporter_notes,json=reporterNotes,proto3" json:"reporter_notes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateReportRequest) Reset() { + *x = CreateReportRequest{} + mi := &file_monkeys_protos_gw_report_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateReportRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateReportRequest) ProtoMessage() {} + +func (x *CreateReportRequest) ProtoReflect() protoreflect.Message { + mi := &file_monkeys_protos_gw_report_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateReportRequest.ProtoReflect.Descriptor instead. +func (*CreateReportRequest) Descriptor() ([]byte, []int) { + return file_monkeys_protos_gw_report_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateReportRequest) GetReasonType() string { + if x != nil { + return x.ReasonType + } + return "" +} + +func (x *CreateReportRequest) GetReporterId() string { + if x != nil { + return x.ReporterId + } + return "" +} + +func (x *CreateReportRequest) GetReportedType() string { + if x != nil { + return x.ReportedType + } + return "" +} + +func (x *CreateReportRequest) GetReportedId() string { + if x != nil { + return x.ReportedId + } + return "" +} + +func (x *CreateReportRequest) GetReporterNotes() string { + if x != nil { + return x.ReporterNotes + } + return "" +} + +type CreateReportResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status uint32 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateReportResponse) Reset() { + *x = CreateReportResponse{} + mi := &file_monkeys_protos_gw_report_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateReportResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateReportResponse) ProtoMessage() {} + +func (x *CreateReportResponse) ProtoReflect() protoreflect.Message { + mi := &file_monkeys_protos_gw_report_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateReportResponse.ProtoReflect.Descriptor instead. +func (*CreateReportResponse) Descriptor() ([]byte, []int) { + return file_monkeys_protos_gw_report_proto_rawDescGZIP(), []int{2} +} + +func (x *CreateReportResponse) GetStatus() uint32 { + if x != nil { + return x.Status + } + return 0 +} + +func (x *CreateReportResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *CreateReportResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type FlagReportRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ReportId string `protobuf:"bytes,1,opt,name=report_id,json=reportId,proto3" json:"report_id,omitempty"` + UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FlagReportRequest) Reset() { + *x = FlagReportRequest{} + mi := &file_monkeys_protos_gw_report_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FlagReportRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FlagReportRequest) ProtoMessage() {} + +func (x *FlagReportRequest) ProtoReflect() protoreflect.Message { + mi := &file_monkeys_protos_gw_report_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FlagReportRequest.ProtoReflect.Descriptor instead. +func (*FlagReportRequest) Descriptor() ([]byte, []int) { + return file_monkeys_protos_gw_report_proto_rawDescGZIP(), []int{3} +} + +func (x *FlagReportRequest) GetReportId() string { + if x != nil { + return x.ReportId + } + return "" +} + +func (x *FlagReportRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +type FlagReportResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status uint32 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FlagReportResponse) Reset() { + *x = FlagReportResponse{} + mi := &file_monkeys_protos_gw_report_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FlagReportResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FlagReportResponse) ProtoMessage() {} + +func (x *FlagReportResponse) ProtoReflect() protoreflect.Message { + mi := &file_monkeys_protos_gw_report_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FlagReportResponse.ProtoReflect.Descriptor instead. +func (*FlagReportResponse) Descriptor() ([]byte, []int) { + return file_monkeys_protos_gw_report_proto_rawDescGZIP(), []int{4} +} + +func (x *FlagReportResponse) GetStatus() uint32 { + if x != nil { + return x.Status + } + return 0 +} + +func (x *FlagReportResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *FlagReportResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +var File_monkeys_protos_gw_report_proto protoreflect.FileDescriptor + +const file_monkeys_protos_gw_report_proto_rawDesc = "" + + "\n" + + "\x1emonkeys_protos/gw_report.proto\x12\vreports_svc\"\xf1\x02\n" + + "\x06Report\x12\x1b\n" + + "\treport_id\x18\x01 \x01(\tR\breportId\x12\x1f\n" + + "\vreason_type\x18\x02 \x01(\tR\n" + + "reasonType\x12\x1d\n" + + "\n" + + "flag_count\x18\x03 \x01(\rR\tflagCount\x12\x1f\n" + + "\vreporter_id\x18\x04 \x01(\tR\n" + + "reporterId\x12#\n" + + "\rreported_type\x18\x05 \x01(\tR\freportedType\x12\x1f\n" + + "\vreported_id\x18\x06 \x01(\tR\n" + + "reportedId\x12\x16\n" + + "\x06status\x18\a \x01(\tR\x06status\x12%\n" + + "\x0ereporter_notes\x18\b \x01(\tR\rreporterNotes\x12!\n" + + "\fmoderator_id\x18\t \x01(\tR\vmoderatorId\x12'\n" + + "\x0fmoderator_notes\x18\n" + + " \x01(\tR\x0emoderatorNotes\x12\x18\n" + + "\averdict\x18\v \x01(\tR\averdict\"\xc4\x01\n" + + "\x13CreateReportRequest\x12\x1f\n" + + "\vreason_type\x18\x01 \x01(\tR\n" + + "reasonType\x12\x1f\n" + + "\vreporter_id\x18\x02 \x01(\tR\n" + + "reporterId\x12#\n" + + "\rreported_type\x18\x03 \x01(\tR\freportedType\x12\x1f\n" + + "\vreported_id\x18\x04 \x01(\tR\n" + + "reportedId\x12%\n" + + "\x0ereporter_notes\x18\x05 \x01(\tR\rreporterNotes\"^\n" + + "\x14CreateReportResponse\x12\x16\n" + + "\x06status\x18\x01 \x01(\rR\x06status\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\x12\x14\n" + + "\x05error\x18\x03 \x01(\tR\x05error\"I\n" + + "\x11FlagReportRequest\x12\x1b\n" + + "\treport_id\x18\x01 \x01(\tR\breportId\x12\x17\n" + + "\auser_id\x18\x02 \x01(\tR\x06userId\"\\\n" + + "\x12FlagReportResponse\x12\x16\n" + + "\x06status\x18\x01 \x01(\rR\x06status\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\x12\x14\n" + + "\x05error\x18\x03 \x01(\tR\x05error2\xb8\x01\n" + + "\x0eReportsService\x12U\n" + + "\fCreateReport\x12 .reports_svc.CreateReportRequest\x1a!.reports_svc.CreateReportResponse\"\x00\x12O\n" + + "\n" + + "FlagReport\x12\x1e.reports_svc.FlagReportRequest\x1a\x1f.reports_svc.FlagReportResponse\"\x00B\x10Z\x0e./generated/gob\x06proto3" + +var ( + file_monkeys_protos_gw_report_proto_rawDescOnce sync.Once + file_monkeys_protos_gw_report_proto_rawDescData []byte +) + +func file_monkeys_protos_gw_report_proto_rawDescGZIP() []byte { + file_monkeys_protos_gw_report_proto_rawDescOnce.Do(func() { + file_monkeys_protos_gw_report_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_monkeys_protos_gw_report_proto_rawDesc), len(file_monkeys_protos_gw_report_proto_rawDesc))) + }) + return file_monkeys_protos_gw_report_proto_rawDescData +} + +var file_monkeys_protos_gw_report_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_monkeys_protos_gw_report_proto_goTypes = []any{ + (*Report)(nil), // 0: reports_svc.Report + (*CreateReportRequest)(nil), // 1: reports_svc.CreateReportRequest + (*CreateReportResponse)(nil), // 2: reports_svc.CreateReportResponse + (*FlagReportRequest)(nil), // 3: reports_svc.FlagReportRequest + (*FlagReportResponse)(nil), // 4: reports_svc.FlagReportResponse +} +var file_monkeys_protos_gw_report_proto_depIdxs = []int32{ + 1, // 0: reports_svc.ReportsService.CreateReport:input_type -> reports_svc.CreateReportRequest + 3, // 1: reports_svc.ReportsService.FlagReport:input_type -> reports_svc.FlagReportRequest + 2, // 2: reports_svc.ReportsService.CreateReport:output_type -> reports_svc.CreateReportResponse + 4, // 3: reports_svc.ReportsService.FlagReport:output_type -> reports_svc.FlagReportResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_monkeys_protos_gw_report_proto_init() } +func file_monkeys_protos_gw_report_proto_init() { + if File_monkeys_protos_gw_report_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_monkeys_protos_gw_report_proto_rawDesc), len(file_monkeys_protos_gw_report_proto_rawDesc)), + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_monkeys_protos_gw_report_proto_goTypes, + DependencyIndexes: file_monkeys_protos_gw_report_proto_depIdxs, + MessageInfos: file_monkeys_protos_gw_report_proto_msgTypes, + }.Build() + File_monkeys_protos_gw_report_proto = out.File + file_monkeys_protos_gw_report_proto_goTypes = nil + file_monkeys_protos_gw_report_proto_depIdxs = nil +} diff --git a/generated/go/gw_report_grpc.pb.go b/generated/go/gw_report_grpc.pb.go new file mode 100644 index 00000000..10fa7c07 --- /dev/null +++ b/generated/go/gw_report_grpc.pb.go @@ -0,0 +1,159 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.32.0 +// source: monkeys_protos/gw_report.proto + +package _go + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ReportsService_CreateReport_FullMethodName = "/reports_svc.ReportsService/CreateReport" + ReportsService_FlagReport_FullMethodName = "/reports_svc.ReportsService/FlagReport" +) + +// ReportsServiceClient is the client API for ReportsService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ReportsServiceClient interface { + CreateReport(ctx context.Context, in *CreateReportRequest, opts ...grpc.CallOption) (*CreateReportResponse, error) + FlagReport(ctx context.Context, in *FlagReportRequest, opts ...grpc.CallOption) (*FlagReportResponse, error) +} + +type reportsServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewReportsServiceClient(cc grpc.ClientConnInterface) ReportsServiceClient { + return &reportsServiceClient{cc} +} + +func (c *reportsServiceClient) CreateReport(ctx context.Context, in *CreateReportRequest, opts ...grpc.CallOption) (*CreateReportResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateReportResponse) + err := c.cc.Invoke(ctx, ReportsService_CreateReport_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *reportsServiceClient) FlagReport(ctx context.Context, in *FlagReportRequest, opts ...grpc.CallOption) (*FlagReportResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(FlagReportResponse) + err := c.cc.Invoke(ctx, ReportsService_FlagReport_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ReportsServiceServer is the server API for ReportsService service. +// All implementations must embed UnimplementedReportsServiceServer +// for forward compatibility. +type ReportsServiceServer interface { + CreateReport(context.Context, *CreateReportRequest) (*CreateReportResponse, error) + FlagReport(context.Context, *FlagReportRequest) (*FlagReportResponse, error) + mustEmbedUnimplementedReportsServiceServer() +} + +// UnimplementedReportsServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedReportsServiceServer struct{} + +func (UnimplementedReportsServiceServer) CreateReport(context.Context, *CreateReportRequest) (*CreateReportResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateReport not implemented") +} +func (UnimplementedReportsServiceServer) FlagReport(context.Context, *FlagReportRequest) (*FlagReportResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method FlagReport not implemented") +} +func (UnimplementedReportsServiceServer) mustEmbedUnimplementedReportsServiceServer() {} +func (UnimplementedReportsServiceServer) testEmbeddedByValue() {} + +// UnsafeReportsServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ReportsServiceServer will +// result in compilation errors. +type UnsafeReportsServiceServer interface { + mustEmbedUnimplementedReportsServiceServer() +} + +func RegisterReportsServiceServer(s grpc.ServiceRegistrar, srv ReportsServiceServer) { + // If the following call pancis, it indicates UnimplementedReportsServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ReportsService_ServiceDesc, srv) +} + +func _ReportsService_CreateReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateReportRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ReportsServiceServer).CreateReport(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ReportsService_CreateReport_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReportsServiceServer).CreateReport(ctx, req.(*CreateReportRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ReportsService_FlagReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FlagReportRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ReportsServiceServer).FlagReport(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ReportsService_FlagReport_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReportsServiceServer).FlagReport(ctx, req.(*FlagReportRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ReportsService_ServiceDesc is the grpc.ServiceDesc for ReportsService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ReportsService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "reports_svc.ReportsService", + HandlerType: (*ReportsServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateReport", + Handler: _ReportsService_CreateReport_Handler, + }, + { + MethodName: "FlagReport", + Handler: _ReportsService_FlagReport_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "monkeys_protos/gw_report.proto", +} diff --git a/microservices/the_monkeys_gateway/internal/reports/routes.go b/microservices/the_monkeys_gateway/internal/reports/routes.go new file mode 100644 index 00000000..31b2ea55 --- /dev/null +++ b/microservices/the_monkeys_gateway/internal/reports/routes.go @@ -0,0 +1,97 @@ +package reports + +import ( + "context" + "fmt" + + "github.com/gin-gonic/gin" + "github.com/the-monkeys/the_monkeys/config" + pb "github.com/the-monkeys/the_monkeys/generated/go" + "github.com/the-monkeys/the_monkeys/microservices/the_monkeys_gateway/internal/auth" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type ReportsServiceClient struct { + Client pb.ReportsServiceClient + config *config.Config + log *zap.SugaredLogger +} + +func NewReportsServiceClient(cfg *config.Config, lg *zap.SugaredLogger) pb.ReportsServiceClient { + addr := fmt.Sprintf("%s:%d", cfg.Microservices.ReportsService, cfg.Microservices.ReportsServicePort) + + conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + lg.Errorw("cannot create gRPC recommendations client", "err", err, "addr", addr) + return nil + } + + lg.Debug("connected to recommendations service", "addr", addr) + return pb.NewReportsServiceClient(conn) +} + +func (rsc *ReportsServiceClient) CreateReport(ctx *gin.Context) { + type requestBody struct { + ReasonType string `json:"reason_type"` + ReporterId string `json:"reporter_id"` + ReportedType string `json:"reported_type"` + ReportedId string `json:"reported_id"` + ReporterNotes string `json:"reporter_notes"` + } + + var body requestBody + if err := ctx.ShouldBindJSON(&body); err != nil { + rsc.log.Error("Error parsing request body", err.Error()) + + ctx.JSON(422, gin.H{ + "status": "error", + "message": "err", + }) + return + } + + rsc.log.Info("Received body: ", body) + + res, err := rsc.Client.CreateReport(context.Background(), &pb.CreateReportRequest{ + ReporterId: body.ReporterId, + ReasonType: body.ReasonType, + ReportedType: body.ReportedType, + ReporterNotes: body.ReporterNotes, + ReportedId: body.ReportedId, + }) + + rsc.log.Info(res) + + if err != nil { + rsc.log.Error(err.Error()) + ctx.JSON(500, gin.H{"error": err.Error()}) + return + } + + if res.Status == 0 { + ctx.JSON(500, gin.H{"error": res.Error}) + return + } + + ctx.JSON(201, gin.H{"success": res.Message}) +} + +func RegisterReportsServiceRoutes(router *gin.Engine, cfg *config.Config, authClient *auth.ServiceClient, lg *zap.SugaredLogger) *ReportsServiceClient { + mware := auth.InitAuthMiddleware(authClient, lg) + + reportsServiceClient := &ReportsServiceClient{ + Client: NewReportsServiceClient(cfg, lg), + config: cfg, + log: lg, + } + + routes := router.Group("/api/v1/reports") + + routes.Use(mware.AuthRequired) + + routes.POST("/", reportsServiceClient.CreateReport) + + return reportsServiceClient +} diff --git a/microservices/the_monkeys_gateway/internal/storage_v2/routes.go b/microservices/the_monkeys_gateway/internal/storage_v2/routes.go index c358d762..07307c94 100644 --- a/microservices/the_monkeys_gateway/internal/storage_v2/routes.go +++ b/microservices/the_monkeys_gateway/internal/storage_v2/routes.go @@ -49,6 +49,7 @@ func newService(cfg *config.Config, log *zap.SugaredLogger) (*Service, error) { Secure: cfg.Minio.UseSSL, }) if err != nil { + log.Error(err.Error()) return nil, err } diff --git a/microservices/the_monkeys_gateway/main.go b/microservices/the_monkeys_gateway/main.go index c530ddab..92b79306 100644 --- a/microservices/the_monkeys_gateway/main.go +++ b/microservices/the_monkeys_gateway/main.go @@ -18,6 +18,7 @@ import ( "github.com/the-monkeys/the_monkeys/microservices/the_monkeys_gateway/internal/blog" "github.com/the-monkeys/the_monkeys/microservices/the_monkeys_gateway/internal/monkeys_ai" "github.com/the-monkeys/the_monkeys/microservices/the_monkeys_gateway/internal/notification" + "github.com/the-monkeys/the_monkeys/microservices/the_monkeys_gateway/internal/reports" "github.com/the-monkeys/the_monkeys/microservices/the_monkeys_gateway/internal/storage" "github.com/the-monkeys/the_monkeys/microservices/the_monkeys_gateway/internal/storage_v2" "github.com/the-monkeys/the_monkeys/microservices/the_monkeys_gateway/internal/systems" @@ -99,6 +100,7 @@ func main() { monkeys_ai.RegisterRecommendationRoute(server.router, cfg, authClient, log) admin.RegisterAdminRouter(server.router, cfg, log) systems.RegisterSystemRouter(server.router, cfg, log) + reports.RegisterReportsServiceRoutes(server.router, cfg, authClient, log) // Health check endpoint server.router.GET("/healthz", func(c *gin.Context) { diff --git a/microservices/the_monkeys_reports/.gitignore b/microservices/the_monkeys_reports/.gitignore new file mode 100644 index 00000000..01a47214 --- /dev/null +++ b/microservices/the_monkeys_reports/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ + +.venv/ diff --git a/microservices/the_monkeys_reports/.python-version b/microservices/the_monkeys_reports/.python-version new file mode 100644 index 00000000..24ee5b1b --- /dev/null +++ b/microservices/the_monkeys_reports/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/microservices/the_monkeys_reports/Dockerfile b/microservices/the_monkeys_reports/Dockerfile new file mode 100644 index 00000000..9c3082ee --- /dev/null +++ b/microservices/the_monkeys_reports/Dockerfile @@ -0,0 +1,28 @@ +FROM ghcr.io/astral-sh/uv:python3.12-alpine +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +ENV UV_COMPILE_BYTECODE=1 +ENV UV_NO_INSTALLER_METADATA=1 +ENV UV_LINK_MODE=copy +ENV UV_TOOL_BIN_DIR=/usr/local/bin + +WORKDIR /app + +RUN apk update && \ + apk add --no-cache \ + libpq-dev \ + gcc + + +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --locked --no-install-project --no-dev + +COPY . /app +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --locked --no-dev + +ENV PATH="/app/.venv/bin:$PATH" + +CMD ["python", "main.py"] diff --git a/microservices/the_monkeys_reports/README.md b/microservices/the_monkeys_reports/README.md new file mode 100644 index 00000000..e69de29b diff --git a/microservices/the_monkeys_reports/docker-compose.yml b/microservices/the_monkeys_reports/docker-compose.yml new file mode 100644 index 00000000..5107069e --- /dev/null +++ b/microservices/the_monkeys_reports/docker-compose.yml @@ -0,0 +1,20 @@ +version: '3.0' + +services: + the_monkeys_reports: + container_name: "the_monkeys_reports" + build: + context: microservices/the_monkeys_reports + args: + MICROSERVICE_REPORTS_SERVICE_INTERNAL_PORT: ${MICROSERVICE_REPORTS_SERVICE_INTERNAL_PORT} + networks: + - monkeys-network + restart: unless-stopped + + ports: + - "${MICROSERVICE_REPORTS_SERVICE_PORT}:${MICROSERVICE_REPORTS_SERVICE_INTERNAL_PORT}" + depends_on: + - the_monkeys_db + env_file: + - .env + diff --git a/microservices/the_monkeys_reports/gw_report_pb2.py b/microservices/the_monkeys_reports/gw_report_pb2.py new file mode 100644 index 00000000..3522d13b --- /dev/null +++ b/microservices/the_monkeys_reports/gw_report_pb2.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: gw_report.proto +# Protobuf Python Version: 6.31.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + 'gw_report.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fgw_report.proto\x12\x0breports_svc\"\xed\x01\n\x06Report\x12\x11\n\treport_id\x18\x01 \x01(\t\x12\x13\n\x0breason_type\x18\x02 \x01(\t\x12\x12\n\nflag_count\x18\x03 \x01(\r\x12\x13\n\x0breporter_id\x18\x04 \x01(\t\x12\x15\n\rreported_type\x18\x05 \x01(\t\x12\x13\n\x0breported_id\x18\x06 \x01(\t\x12\x0e\n\x06status\x18\x07 \x01(\t\x12\x16\n\x0ereporter_notes\x18\x08 \x01(\t\x12\x14\n\x0cmoderator_id\x18\t \x01(\t\x12\x17\n\x0fmoderator_notes\x18\n \x01(\t\x12\x0f\n\x07verdict\x18\x0b \x01(\t\"\x83\x01\n\x13\x43reateReportRequest\x12\x13\n\x0breason_type\x18\x01 \x01(\t\x12\x13\n\x0breporter_id\x18\x02 \x01(\t\x12\x15\n\rreported_type\x18\x03 \x01(\t\x12\x13\n\x0breported_id\x18\x04 \x01(\t\x12\x16\n\x0ereporter_notes\x18\x05 \x01(\t\"F\n\x14\x43reateReportResponse\x12\x0e\n\x06status\x18\x01 \x01(\r\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\r\n\x05\x65rror\x18\x03 \x01(\t\"7\n\x11\x46lagReportRequest\x12\x11\n\treport_id\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\"D\n\x12\x46lagReportResponse\x12\x0e\n\x06status\x18\x01 \x01(\r\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\r\n\x05\x65rror\x18\x03 \x01(\t2\xb8\x01\n\x0eReportsService\x12U\n\x0c\x43reateReport\x12 .reports_svc.CreateReportRequest\x1a!.reports_svc.CreateReportResponse\"\x00\x12O\n\nFlagReport\x12\x1e.reports_svc.FlagReportRequest\x1a\x1f.reports_svc.FlagReportResponse\"\x00\x42\x10Z\x0e./generated/gob\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'gw_report_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z\016./generated/go' + _globals['_REPORT']._serialized_start=33 + _globals['_REPORT']._serialized_end=270 + _globals['_CREATEREPORTREQUEST']._serialized_start=273 + _globals['_CREATEREPORTREQUEST']._serialized_end=404 + _globals['_CREATEREPORTRESPONSE']._serialized_start=406 + _globals['_CREATEREPORTRESPONSE']._serialized_end=476 + _globals['_FLAGREPORTREQUEST']._serialized_start=478 + _globals['_FLAGREPORTREQUEST']._serialized_end=533 + _globals['_FLAGREPORTRESPONSE']._serialized_start=535 + _globals['_FLAGREPORTRESPONSE']._serialized_end=603 + _globals['_REPORTSSERVICE']._serialized_start=606 + _globals['_REPORTSSERVICE']._serialized_end=790 +# @@protoc_insertion_point(module_scope) diff --git a/microservices/the_monkeys_reports/gw_report_pb2.pyi b/microservices/the_monkeys_reports/gw_report_pb2.pyi new file mode 100644 index 00000000..bfabd8f7 --- /dev/null +++ b/microservices/the_monkeys_reports/gw_report_pb2.pyi @@ -0,0 +1,74 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class Report(_message.Message): + __slots__ = ("report_id", "reason_type", "flag_count", "reporter_id", "reported_type", "reported_id", "status", "reporter_notes", "moderator_id", "moderator_notes", "verdict") + REPORT_ID_FIELD_NUMBER: _ClassVar[int] + REASON_TYPE_FIELD_NUMBER: _ClassVar[int] + FLAG_COUNT_FIELD_NUMBER: _ClassVar[int] + REPORTER_ID_FIELD_NUMBER: _ClassVar[int] + REPORTED_TYPE_FIELD_NUMBER: _ClassVar[int] + REPORTED_ID_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + REPORTER_NOTES_FIELD_NUMBER: _ClassVar[int] + MODERATOR_ID_FIELD_NUMBER: _ClassVar[int] + MODERATOR_NOTES_FIELD_NUMBER: _ClassVar[int] + VERDICT_FIELD_NUMBER: _ClassVar[int] + report_id: str + reason_type: str + flag_count: int + reporter_id: str + reported_type: str + reported_id: str + status: str + reporter_notes: str + moderator_id: str + moderator_notes: str + verdict: str + def __init__(self, report_id: _Optional[str] = ..., reason_type: _Optional[str] = ..., flag_count: _Optional[int] = ..., reporter_id: _Optional[str] = ..., reported_type: _Optional[str] = ..., reported_id: _Optional[str] = ..., status: _Optional[str] = ..., reporter_notes: _Optional[str] = ..., moderator_id: _Optional[str] = ..., moderator_notes: _Optional[str] = ..., verdict: _Optional[str] = ...) -> None: ... + +class CreateReportRequest(_message.Message): + __slots__ = ("reason_type", "reporter_id", "reported_type", "reported_id", "reporter_notes") + REASON_TYPE_FIELD_NUMBER: _ClassVar[int] + REPORTER_ID_FIELD_NUMBER: _ClassVar[int] + REPORTED_TYPE_FIELD_NUMBER: _ClassVar[int] + REPORTED_ID_FIELD_NUMBER: _ClassVar[int] + REP: + ORTER_NOTES_FIELD_NUMBER: _ClassVar[int] + reason_type: str + reporter_id: str + reported_type: str + reported_id: str + reporter_notes: str + def __init__(self, reason_type: _Optional[str] = ..., reporter_id: _Optional[str] = ..., reported_type: _Optional[str] = ..., reported_id: _Optional[str] = ..., reporter_notes: _Optional[str] = ...) -> None: ... + +class CreateReportResponse(_message.Message): + __slots__ = ("status", "message", "error") + STATUS_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + status: int + message: str + error: str + def __init__(self, status: _Optional[int] = ..., message: _Optional[str] = ..., error: _Optional[str] = ...) -> None: ... + +class FlagReportRequest(_message.Message): + __slots__ = ("report_id", "user_id") + REPORT_ID_FIELD_NUMBER: _ClassVar[int] + USER_ID_FIELD_NUMBER: _ClassVar[int] + report_id: str + user_id: str + def __init__(self, report_id: _Optional[str] = ..., user_id: _Optional[str] = ...) -> None: ... + +class FlagReportResponse(_message.Message): + __slots__ = ("status", "message", "error") + STATUS_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + status: int + message: str + error: str + def __init__(self, status: _Optional[int] = ..., message: _Optional[str] = ..., error: _Optional[str] = ...) -> None: ... diff --git a/microservices/the_monkeys_reports/gw_report_pb2_grpc.py b/microservices/the_monkeys_reports/gw_report_pb2_grpc.py new file mode 100644 index 00000000..0639cba8 --- /dev/null +++ b/microservices/the_monkeys_reports/gw_report_pb2_grpc.py @@ -0,0 +1,140 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +import gw_report_pb2 as gw__report__pb2 + +GRPC_GENERATED_VERSION = '1.75.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in gw_report_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class ReportsServiceStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.CreateReport = channel.unary_unary( + '/reports_svc.ReportsService/CreateReport', + request_serializer=gw__report__pb2.CreateReportRequest.SerializeToString, + response_deserializer=gw__report__pb2.CreateReportResponse.FromString, + _registered_method=True) + self.FlagReport = channel.unary_unary( + '/reports_svc.ReportsService/FlagReport', + request_serializer=gw__report__pb2.FlagReportRequest.SerializeToString, + response_deserializer=gw__report__pb2.FlagReportResponse.FromString, + _registered_method=True) + + +class ReportsServiceServicer(object): + """Missing associated documentation comment in .proto file.""" + + def CreateReport(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 FlagReport(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_ReportsServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'CreateReport': grpc.unary_unary_rpc_method_handler( + servicer.CreateReport, + request_deserializer=gw__report__pb2.CreateReportRequest.FromString, + response_serializer=gw__report__pb2.CreateReportResponse.SerializeToString, + ), + 'FlagReport': grpc.unary_unary_rpc_method_handler( + servicer.FlagReport, + request_deserializer=gw__report__pb2.FlagReportRequest.FromString, + response_serializer=gw__report__pb2.FlagReportResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'reports_svc.ReportsService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('reports_svc.ReportsService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class ReportsService(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def CreateReport(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, + '/reports_svc.ReportsService/CreateReport', + gw__report__pb2.CreateReportRequest.SerializeToString, + gw__report__pb2.CreateReportResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def FlagReport(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, + '/reports_svc.ReportsService/FlagReport', + gw__report__pb2.FlagReportRequest.SerializeToString, + gw__report__pb2.FlagReportResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/microservices/the_monkeys_reports/main.py b/microservices/the_monkeys_reports/main.py new file mode 100644 index 00000000..38d885aa --- /dev/null +++ b/microservices/the_monkeys_reports/main.py @@ -0,0 +1,82 @@ +import concurrent.futures +import logging +import os +import sys + +import grpc +from sqlalchemy.exc import IntegrityError, SQLAlchemyError + +import gw_report_pb2 +import gw_report_pb2_grpc + +import models +from gw_report_pb2 import CreateReportResponse +from models import Session, DB + +MICROSERVICE_REPORTS_SERVICE_INTERNAL_PORT = os.getenv("MICROSERVICE_REPORTS_SERVICE_INTERNAL_PORT") +MICROSERVICE_REPORTS_SERVICE_PORT = os.getenv("MICROSERVICE_REPORTS_SERVICE_PORT") +APP_ENV = os.getenv("APP_ENV", "development") + + +if APP_ENV == "development": + logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO if APP_ENV == "development" else logging.WARN) + +handler = logging.StreamHandler(stream=sys.stdout) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + +handler.setFormatter(formatter) + +logger.addHandler(handler) + + +class ReportsServiceServicer(gw_report_pb2_grpc.ReportsServiceServicer): + def CreateReport(self, request: gw_report_pb2.CreateReportRequest, context) -> CreateReportResponse | None: + logger.info(f"Received request\n{request}") + + report_body = { + "reporter_id": int(request.reporter_id), + "reason_type": request.reason_type, + "reporter_notes": request.reporter_notes, + "reported_type": request.reported_type + } + + if request.reported_type == "BLOG": + report_body["reported_blog_id"] = int(request.reported_id) + else: + report_body["reported_user_id"] = int(request.reported_id) + + new_report = models.Report(**report_body) + + try: + with Session() as session: + session.add(new_report) + session.commit() + except IntegrityError as e: + logger.error(e) + return gw_report_pb2.CreateReportResponse(status=0, message="", error=e._message()) + except SQLAlchemyError as e: + logger.error(e) + return gw_report_pb2.CreateReportResponse(status=0, message="", error=e._message()) + else: + logger.info("Response sent") + return gw_report_pb2.CreateReportResponse(status=1, message="Created report successfully", error="") + + def FlagReport(self, request, context): + pass + + +def serve(): + server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=20)) + server.add_insecure_port(f"[::]:{MICROSERVICE_REPORTS_SERVICE_INTERNAL_PORT}") + gw_report_pb2_grpc.add_ReportsServiceServicer_to_server(ReportsServiceServicer(), server) + + server.start() + server.wait_for_termination() + +if __name__ == "__main__": + logger.info(f"Starting reports service grpc server on {MICROSERVICE_REPORTS_SERVICE_PORT}") + logger.info(f"Connecting to DB with credentials {DB}") + serve() diff --git a/microservices/the_monkeys_reports/models.py b/microservices/the_monkeys_reports/models.py new file mode 100644 index 00000000..19e80fa5 --- /dev/null +++ b/microservices/the_monkeys_reports/models.py @@ -0,0 +1,62 @@ +from sqlalchemy import String, INT, ForeignKey, CheckConstraint, MetaData, Table, create_engine +from sqlalchemy.dialects.mysql import BIGINT +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, sessionmaker + +import os + +DB = { + "HOST": os.getenv("POSTGRESQL_PRIMARY_DB_DB_HOST"), + "PASSWORD": os.getenv("POSTGRESQL_PRIMARY_DB_DB_PASSWORD"), + "USERNAME": os.getenv("POSTGRESQL_PRIMARY_DB_DB_USERNAME"), + "NAME": os.getenv("POSTGRESQL_PRIMARY_DB_DB_NAME") +} + +engine = create_engine(f"postgresql+psycopg2://{DB["USERNAME"]}:{DB["PASSWORD"]}@{DB["HOST"]}/{DB["NAME"]}", + pool_size=20) + +Session = sessionmaker(bind=engine) + +metadata = MetaData() + +blog = Table("blog", metadata, autoload_with=engine) +user_account = Table("user_account", metadata, autoload_with=engine) + +class Base(DeclarativeBase): + metadata = metadata + pass + + +class Report(Base): + __tablename__ = "reports" + + report_id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + reason_type: Mapped[str] = mapped_column(String) + flag_count: Mapped[int] = mapped_column(INT, default=0) + reporter_id: Mapped[BIGINT] = mapped_column(ForeignKey("user_account.id")) + + reported_type: Mapped[str] = mapped_column(String) + + reported_blog_id: Mapped[BIGINT] = mapped_column(ForeignKey("blog.id"), nullable=True) + reported_user_id: Mapped[BIGINT] = mapped_column(ForeignKey("user_account.id"), nullable=True) + + status: Mapped[str] = mapped_column(String) + reporter_notes: Mapped[str] = mapped_column(String) + moderator_id: Mapped[BIGINT] = mapped_column(ForeignKey("user_account.id")) + moderator_notes: Mapped[str] = mapped_column(String) + + verdict: Mapped[str] = mapped_column(String) + + __table_args__ = ( + CheckConstraint("status IN ('PENDING', 'IN_PROGRESS', 'RESOLVED')"), + CheckConstraint("reported_type IN ('BLOG', 'USER')"), + CheckConstraint("reason_type IN ('SPAM', 'ABUSE', 'NSFW', 'MISINFORMATION', 'OTHER')"), + CheckConstraint("(reported_blog_id) IS NOT NULL) + (reported_user_id IS NOT NULL) = 1", name="exactly_one_fk") + ) + + +# Association table for user and reports +class UserReportFlag(Base): + __tablename__ = "user_reports_flags" + + user_id: Mapped[BIGINT] = mapped_column(ForeignKey("user_account.id"), primary_key=True) + report_id: Mapped[str] = mapped_column(ForeignKey("reports.report_id"), primary_key=True) diff --git a/microservices/the_monkeys_reports/pyproject.toml b/microservices/the_monkeys_reports/pyproject.toml new file mode 100644 index 00000000..9eab44d6 --- /dev/null +++ b/microservices/the_monkeys_reports/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "the-monkeys-reports" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "sqlalchemy>=2.0.43", + "grpcio-tools>=1.75.1", + "psycopg2-binary>=2.9.11", +] diff --git a/microservices/the_monkeys_reports/uv.lock b/microservices/the_monkeys_reports/uv.lock new file mode 100644 index 00000000..22b82d8b --- /dev/null +++ b/microservices/the_monkeys_reports/uv.lock @@ -0,0 +1,187 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +] + +[[package]] +name = "grpcio" +version = "1.75.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/f7/8963848164c7604efb3a3e6ee457fdb3a469653e19002bd24742473254f8/grpcio-1.75.1.tar.gz", hash = "sha256:3e81d89ece99b9ace23a6916880baca613c03a799925afb2857887efa8b1b3d2", size = 12731327, upload-time = "2025-09-26T09:03:36.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/74/bac4ab9f7722164afdf263ae31ba97b8174c667153510322a5eba4194c32/grpcio-1.75.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:3bed22e750d91d53d9e31e0af35a7b0b51367e974e14a4ff229db5b207647884", size = 5672779, upload-time = "2025-09-26T09:02:19.11Z" }, + { url = "https://files.pythonhosted.org/packages/a6/52/d0483cfa667cddaa294e3ab88fd2c2a6e9dc1a1928c0e5911e2e54bd5b50/grpcio-1.75.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5b8f381eadcd6ecaa143a21e9e80a26424c76a0a9b3d546febe6648f3a36a5ac", size = 11470623, upload-time = "2025-09-26T09:02:22.117Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e4/d1954dce2972e32384db6a30273275e8c8ea5a44b80347f9055589333b3f/grpcio-1.75.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5bf4001d3293e3414d0cf99ff9b1139106e57c3a66dfff0c5f60b2a6286ec133", size = 6248838, upload-time = "2025-09-26T09:02:26.426Z" }, + { url = "https://files.pythonhosted.org/packages/06/43/073363bf63826ba8077c335d797a8d026f129dc0912b69c42feaf8f0cd26/grpcio-1.75.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f82ff474103e26351dacfe8d50214e7c9322960d8d07ba7fa1d05ff981c8b2d", size = 6922663, upload-time = "2025-09-26T09:02:28.724Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6f/076ac0df6c359117676cacfa8a377e2abcecec6a6599a15a672d331f6680/grpcio-1.75.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0ee119f4f88d9f75414217823d21d75bfe0e6ed40135b0cbbfc6376bc9f7757d", size = 6436149, upload-time = "2025-09-26T09:02:30.971Z" }, + { url = "https://files.pythonhosted.org/packages/6b/27/1d08824f1d573fcb1fa35ede40d6020e68a04391709939e1c6f4193b445f/grpcio-1.75.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:664eecc3abe6d916fa6cf8dd6b778e62fb264a70f3430a3180995bf2da935446", size = 7067989, upload-time = "2025-09-26T09:02:33.233Z" }, + { url = "https://files.pythonhosted.org/packages/c6/98/98594cf97b8713feb06a8cb04eeef60b4757e3e2fb91aa0d9161da769843/grpcio-1.75.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c32193fa08b2fbebf08fe08e84f8a0aad32d87c3ad42999c65e9449871b1c66e", size = 8010717, upload-time = "2025-09-26T09:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7e/bb80b1bba03c12158f9254762cdf5cced4a9bc2e8ed51ed335915a5a06ef/grpcio-1.75.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5cebe13088b9254f6e615bcf1da9131d46cfa4e88039454aca9cb65f639bd3bc", size = 7463822, upload-time = "2025-09-26T09:02:38.26Z" }, + { url = "https://files.pythonhosted.org/packages/23/1c/1ea57fdc06927eb5640f6750c697f596f26183573069189eeaf6ef86ba2d/grpcio-1.75.1-cp313-cp313-win32.whl", hash = "sha256:4b4c678e7ed50f8ae8b8dbad15a865ee73ce12668b6aaf411bf3258b5bc3f970", size = 3938490, upload-time = "2025-09-26T09:02:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/4b/24/fbb8ff1ccadfbf78ad2401c41aceaf02b0d782c084530d8871ddd69a2d49/grpcio-1.75.1-cp313-cp313-win_amd64.whl", hash = "sha256:5573f51e3f296a1bcf71e7a690c092845fb223072120f4bdb7a5b48e111def66", size = 4642538, upload-time = "2025-09-26T09:02:42.519Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1b/9a0a5cecd24302b9fdbcd55d15ed6267e5f3d5b898ff9ac8cbe17ee76129/grpcio-1.75.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:c05da79068dd96723793bffc8d0e64c45f316248417515f28d22204d9dae51c7", size = 5673319, upload-time = "2025-09-26T09:02:44.742Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ec/9d6959429a83fbf5df8549c591a8a52bb313976f6646b79852c4884e3225/grpcio-1.75.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06373a94fd16ec287116a825161dca179a0402d0c60674ceeec8c9fba344fe66", size = 11480347, upload-time = "2025-09-26T09:02:47.539Z" }, + { url = "https://files.pythonhosted.org/packages/09/7a/26da709e42c4565c3d7bf999a9569da96243ce34a8271a968dee810a7cf1/grpcio-1.75.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4484f4b7287bdaa7a5b3980f3c7224c3c622669405d20f69549f5fb956ad0421", size = 6254706, upload-time = "2025-09-26T09:02:50.4Z" }, + { url = "https://files.pythonhosted.org/packages/f1/08/dcb26a319d3725f199c97e671d904d84ee5680de57d74c566a991cfab632/grpcio-1.75.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2720c239c1180eee69f7883c1d4c83fc1a495a2535b5fa322887c70bf02b16e8", size = 6922501, upload-time = "2025-09-26T09:02:52.711Z" }, + { url = "https://files.pythonhosted.org/packages/78/66/044d412c98408a5e23cb348845979a2d17a2e2b6c3c34c1ec91b920f49d0/grpcio-1.75.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:07a554fa31c668cf0e7a188678ceeca3cb8fead29bbe455352e712ec33ca701c", size = 6437492, upload-time = "2025-09-26T09:02:55.542Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9d/5e3e362815152aa1afd8b26ea613effa005962f9da0eec6e0e4527e7a7d1/grpcio-1.75.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3e71a2105210366bfc398eef7f57a664df99194f3520edb88b9c3a7e46ee0d64", size = 7081061, upload-time = "2025-09-26T09:02:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1a/46615682a19e100f46e31ddba9ebc297c5a5ab9ddb47b35443ffadb8776c/grpcio-1.75.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8679aa8a5b67976776d3c6b0521e99d1c34db8a312a12bcfd78a7085cb9b604e", size = 8010849, upload-time = "2025-09-26T09:03:00.548Z" }, + { url = "https://files.pythonhosted.org/packages/67/8e/3204b94ac30b0f675ab1c06540ab5578660dc8b690db71854d3116f20d00/grpcio-1.75.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aad1c774f4ebf0696a7f148a56d39a3432550612597331792528895258966dc0", size = 7464478, upload-time = "2025-09-26T09:03:03.096Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/2d90652b213863b2cf466d9c1260ca7e7b67a16780431b3eb1d0420e3d5b/grpcio-1.75.1-cp314-cp314-win32.whl", hash = "sha256:62ce42d9994446b307649cb2a23335fa8e927f7ab2cbf5fcb844d6acb4d85f9c", size = 4012672, upload-time = "2025-09-26T09:03:05.477Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/e2e6e9fc1c985cd1a59e6996a05647c720fe8a03b92f5ec2d60d366c531e/grpcio-1.75.1-cp314-cp314-win_amd64.whl", hash = "sha256:f86e92275710bea3000cb79feca1762dc0ad3b27830dd1a74e82ab321d4ee464", size = 4772475, upload-time = "2025-09-26T09:03:07.661Z" }, +] + +[[package]] +name = "grpcio-tools" +version = "1.75.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/76/0cd2a2bb379275c319544a3ab613dc3cea7a167503908c1b4de55f82bd9e/grpcio_tools-1.75.1.tar.gz", hash = "sha256:bb78960cf3d58941e1fec70cbdaccf255918beed13c34112a6915a6d8facebd1", size = 5390470, upload-time = "2025-09-26T09:10:11.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/fa/624bbe1b2ccf4f6044bf3cd314fe2c35f78f702fcc2191dc65519baddca4/grpcio_tools-1.75.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:ca9e116aab0ecf4365fc2980f2e8ae1b22273c3847328b9a8e05cbd14345b397", size = 2545752, upload-time = "2025-09-26T09:08:51.433Z" }, + { url = "https://files.pythonhosted.org/packages/b9/4c/6d884e2337feff0a656e395338019adecc3aa1daeae9d7e8eb54340d4207/grpcio_tools-1.75.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:9fe87a926b65eb7f41f8738b6d03677cc43185ff77a9d9b201bdb2f673f3fa1e", size = 5838163, upload-time = "2025-09-26T09:08:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2a/2ba7b6911a754719643ed92ae816a7f989af2be2882b9a9e1f90f4b0e882/grpcio_tools-1.75.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:45503a6094f91b3fd31c3d9adef26ac514f102086e2a37de797e220a6791ee87", size = 2592148, upload-time = "2025-09-26T09:08:55.86Z" }, + { url = "https://files.pythonhosted.org/packages/88/db/fa613a45c3c7b00f905bd5ad3a93c73194724d0a2dd72adae3be32983343/grpcio_tools-1.75.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b01b60b3de67be531a39fd869d7613fa8f178aff38c05e4d8bc2fc530fa58cb5", size = 2905215, upload-time = "2025-09-26T09:08:58.27Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0c/ee4786972bb82f60e4f313bb2227c79c2cd20eb13c94c0263067923cfd12/grpcio_tools-1.75.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e2b9b9488735514777d44c1e4eda813122d2c87aad219f98d5d49b359a8eab", size = 2656251, upload-time = "2025-09-26T09:09:00.249Z" }, + { url = "https://files.pythonhosted.org/packages/77/f1/cc5a50658d705d0b71ff8a4fbbfcc6279d3c95731a2ef7285e13dc40e2fe/grpcio_tools-1.75.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:55e60300e62b220fabe6f062fe69f143abaeff3335f79b22b56d86254f3c3c80", size = 3108911, upload-time = "2025-09-26T09:09:02.515Z" }, + { url = "https://files.pythonhosted.org/packages/09/d8/43545f77c4918e778e90bc2c02b3462ac71cee14f29d85cdb69b089538eb/grpcio_tools-1.75.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:49ce00fcc6facbbf52bf376e55b8e08810cecd03dab0b3a2986d73117c6f6ee4", size = 3657021, upload-time = "2025-09-26T09:09:05.331Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0b/2ae5925374b66bc8df5b828eff1a5f9459349c83dae1773f0aa9858707e6/grpcio_tools-1.75.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71e95479aea868f8c8014d9dc4267f26ee75388a0d8a552e1648cfa0b53d24b4", size = 3324450, upload-time = "2025-09-26T09:09:07.867Z" }, + { url = "https://files.pythonhosted.org/packages/6e/53/9f887bacbecf892ac5b0b282477ca8cfa5b73911b04259f0d88b52e9a055/grpcio_tools-1.75.1-cp313-cp313-win32.whl", hash = "sha256:fff9d2297416eae8861e53154ccf70a19994e5935e6c8f58ebf431f81cbd8d12", size = 992434, upload-time = "2025-09-26T09:09:09.966Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f0/9979d97002edffdc2a88e5f2e0dccea396dd4a6eab34fa2f705fe43eae2f/grpcio_tools-1.75.1-cp313-cp313-win_amd64.whl", hash = "sha256:1849ddd508143eb48791e81d42ddc924c554d1b4900e06775a927573a8d4267f", size = 1157069, upload-time = "2025-09-26T09:09:12.287Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0b/4ff4ead293f2b016668628a240937828444094778c8037d2bbef700e9097/grpcio_tools-1.75.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:f281b594489184b1f9a337cdfed1fc1ddb8428f41c4b4023de81527e90b38e1e", size = 2545868, upload-time = "2025-09-26T09:09:14.716Z" }, + { url = "https://files.pythonhosted.org/packages/0e/78/aa6bf73a18de5357c01ef87eea92150931586b25196fa4df197a37bae11d/grpcio_tools-1.75.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:becf8332f391abc62bf4eea488b63be063d76a7cf2ef00b2e36c617d9ee9216b", size = 5838010, upload-time = "2025-09-26T09:09:20.415Z" }, + { url = "https://files.pythonhosted.org/packages/99/65/7eaad673bc971af45e079d3b13c20d9ba9842b8788d31953e3234c2e2cee/grpcio_tools-1.75.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a08330f24e5cd7b39541882a95a8ba04ffb4df79e2984aa0cd01ed26dcdccf49", size = 2593170, upload-time = "2025-09-26T09:09:22.889Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/57e1e29e9186c7ed223ce8a9b609d3f861c4db015efb643dfe60b403c137/grpcio_tools-1.75.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6bf3742bd8f102630072ed317d1496f31c454cd85ad19d37a68bd85bf9d5f8b9", size = 2905167, upload-time = "2025-09-26T09:09:25.96Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7b/894f891f3cf19812192f8bbf1e0e1c958055676ecf0a5466a350730a006d/grpcio_tools-1.75.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f26028949474feb380460ce52d9d090d00023940c65236294a66c42ac5850e8b", size = 2656210, upload-time = "2025-09-26T09:09:28.786Z" }, + { url = "https://files.pythonhosted.org/packages/99/76/8e48427da93ef243c09629969c7b5a2c59dceb674b6b623c1f5fbaa5c8c5/grpcio_tools-1.75.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1bd68fb98bf08f11b6c3210834a14eefe585bad959bdba38e78b4ae3b04ba5bd", size = 3109226, upload-time = "2025-09-26T09:09:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7e/ecf71c316c2a88c2478b7c6372d0f82d05f07edbf0f31b6da613df99ec7c/grpcio_tools-1.75.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f1496e21586193da62c3a73cd16f9c63c5b3efd68ff06dab96dbdfefa90d40bf", size = 3657139, upload-time = "2025-09-26T09:09:35.043Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f3/b2613e81da2085f40a989c0601ec9efc11e8b32fcb71b1234b64a18af830/grpcio_tools-1.75.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:14a78b1e36310cdb3516cdf9ee2726107875e0b247e2439d62fc8dc38cf793c1", size = 3324513, upload-time = "2025-09-26T09:09:37.44Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1f/2df4fa8634542524bc22442ffe045d41905dae62cc5dd14408b80c5ac1b8/grpcio_tools-1.75.1-cp314-cp314-win32.whl", hash = "sha256:0e6f916daf222002fb98f9a6f22de0751959e7e76a24941985cc8e43cea77b50", size = 1015283, upload-time = "2025-09-26T09:09:39.461Z" }, + { url = "https://files.pythonhosted.org/packages/23/4f/f27c973ff50486a70be53a3978b6b0244398ca170a4e19d91988b5295d92/grpcio_tools-1.75.1-cp314-cp314-win_amd64.whl", hash = "sha256:878c3b362264588c45eba57ce088755f8b2b54893d41cc4a68cdeea62996da5c", size = 1189364, upload-time = "2025-09-26T09:09:42.036Z" }, +] + +[[package]] +name = "protobuf" +version = "6.32.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/a4/cc17347aa2897568beece2e674674359f911d6fe21b0b8d6268cd42727ac/protobuf-6.32.1.tar.gz", hash = "sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d", size = 440635, upload-time = "2025-09-11T21:38:42.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/98/645183ea03ab3995d29086b8bf4f7562ebd3d10c9a4b14ee3f20d47cfe50/protobuf-6.32.1-cp310-abi3-win32.whl", hash = "sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085", size = 424411, upload-time = "2025-09-11T21:38:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f3/6f58f841f6ebafe076cebeae33fc336e900619d34b1c93e4b5c97a81fdfa/protobuf-6.32.1-cp310-abi3-win_amd64.whl", hash = "sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1", size = 435738, upload-time = "2025-09-11T21:38:30.959Z" }, + { url = "https://files.pythonhosted.org/packages/10/56/a8a3f4e7190837139e68c7002ec749190a163af3e330f65d90309145a210/protobuf-6.32.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281", size = 426454, upload-time = "2025-09-11T21:38:34.076Z" }, + { url = "https://files.pythonhosted.org/packages/3f/be/8dd0a927c559b37d7a6c8ab79034fd167dcc1f851595f2e641ad62be8643/protobuf-6.32.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4", size = 322874, upload-time = "2025-09-11T21:38:35.509Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f6/88d77011b605ef979aace37b7703e4eefad066f7e84d935e5a696515c2dd/protobuf-6.32.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710", size = 322013, upload-time = "2025-09-11T21:38:37.017Z" }, + { url = "https://files.pythonhosted.org/packages/97/b7/15cc7d93443d6c6a84626ae3258a91f4c6ac8c0edd5df35ea7658f71b79c/protobuf-6.32.1-py3-none-any.whl", hash = "sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346", size = 169289, upload-time = "2025-09-11T21:38:41.234Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/1c/a7260bd47a6fae7e03768bf66451437b36451143f36b285522b865987ced/sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", size = 2130598, upload-time = "2025-08-11T15:51:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/8e/84/8a337454e82388283830b3586ad7847aa9c76fdd4f1df09cdd1f94591873/sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", size = 2118415, upload-time = "2025-08-11T15:51:17.256Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ff/22ab2328148492c4d71899d62a0e65370ea66c877aea017a244a35733685/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", size = 3248707, upload-time = "2025-08-11T15:52:38.444Z" }, + { url = "https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", size = 3253602, upload-time = "2025-08-11T15:56:37.348Z" }, + { url = "https://files.pythonhosted.org/packages/b8/61/987b6c23b12c56d2be451bc70900f67dd7d989d52b1ee64f239cf19aec69/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", size = 3183248, upload-time = "2025-08-11T15:52:39.865Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/29d216002d4593c2ce1c0ec2cec46dda77bfbcd221e24caa6e85eff53d89/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", size = 3219363, upload-time = "2025-08-11T15:56:39.11Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e4/bd78b01919c524f190b4905d47e7630bf4130b9f48fd971ae1c6225b6f6a/sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", size = 2096718, upload-time = "2025-08-11T15:55:05.349Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a5/ca2f07a2a201f9497de1928f787926613db6307992fe5cda97624eb07c2f/sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", size = 2123200, upload-time = "2025-08-11T15:55:07.932Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" }, +] + +[[package]] +name = "the-monkeys-reports" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "grpcio-tools" }, + { name = "psycopg2-binary" }, + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [ + { name = "grpcio-tools", specifier = ">=1.75.1" }, + { name = "psycopg2-binary", specifier = ">=2.9.11" }, + { name = "sqlalchemy", specifier = ">=2.0.43" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] diff --git a/monkeys_protos b/monkeys_protos new file mode 160000 index 00000000..f027dbdc --- /dev/null +++ b/monkeys_protos @@ -0,0 +1 @@ +Subproject commit f027dbdc19e0b5f5e274bc12e043568d5c3c2700 diff --git a/schema/000002_create_reports_table.down.sql b/schema/000002_create_reports_table.down.sql new file mode 100644 index 00000000..eb6cba85 --- /dev/null +++ b/schema/000002_create_reports_table.down.sql @@ -0,0 +1,3 @@ +DROP INDEX idx_active_user_reports; +DROP Table report_flags; +DROP Table reports; diff --git a/schema/000002_create_reports_table.up.sql b/schema/000002_create_reports_table.up.sql new file mode 100644 index 00000000..568db6f6 --- /dev/null +++ b/schema/000002_create_reports_table.up.sql @@ -0,0 +1,44 @@ +CREATE TABLE IF NOT EXISTS reports ( + report_id BIGSERIAL PRIMARY KEY, + reason_type VARCHAR(255) NOT NULL, + flag_count INTEGER NOT NULL DEFAULT 0, + reporter_id INTEGER NOT NULL, + reported_type VARCHAR(255), + reported_blog_id BIGINT, + reported_user_id BIGINT, + status VARCHAR(255) DEFAULT 'PENDING', + reporter_notes TEXT, + moderator_id BIGINT, + moderator_notes TEXT, + verdict VARCHAR(255), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (reporter_id) REFERENCES user_account(id) ON DELETE CASCADE, + FOREIGN KEY (reported_user_id) REFERENCES user_account(id) ON DELETE CASCADE, + FOREIGN KEY (reported_blog_id) REFERENCES blog(id) ON DELETE CASCADE, + FOREIGN KEY (moderator_id) REFERENCES user_account(id) ON DELETE CASCADE, + + CONSTRAINT reported_type_values CHECK (reported_type IN ('BLOG', 'USER')), + CONSTRAINT report_reason_types CHECK (reason_type IN ('SPAM', 'ABUSE', 'NSFW', 'MISINFORMATION', 'OTHER')), + CONSTRAINT report_status_types CHECK (status IN ('PENDING', 'IN_PROGRESS', 'RESOLVED')), + CONSTRAINT verdict_types CHECK (verdict IN ('BANNED', 'DELETED', 'IGNORE')), + CONSTRAINT exactly_one_fk CHECK ((reported_blog_id IS NOT NULL) OR (reported_user_id IS NOT NULL)), + CONSTRAINT no_report_self CHECK (reporter_id != reported_user_id) +); + +-- This index makes sure user can't create more more reports that are pending and in progress +CREATE UNIQUE INDEX idx_active_user_reports +ON reports (reporter_id, reported_blog_id, reported_user_id) +WHERE status IN ('PENDING', 'IN_PROGRESS'); + +CREATE TABLE IF NOT EXISTS report_flags ( + report_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + flagged_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY (report_id, user_id), + + FOREIGN KEY (report_id) REFERENCES reports(report_id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES user_account(id) ON DELETE CASCADE +);