Skip to content

Commit 92a4a47

Browse files
authored
chore/production-readiness (#119)
* chore: remove `home` and `navigation` modules * feat: add downstream service for `config` module * refactor: remove default environment variable for settings module * refactor: house cleaning and some minor warning resolutions * refactor: update docker compose files and scripts
1 parent 6acfefc commit 92a4a47

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+609
-449
lines changed

.dockerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*.sqlite3
1111
*.sqlite3-journal
1212
__pycache__/
13+
migrations/
1314

1415
# General Directories
1516
.idea
@@ -29,3 +30,7 @@ pytest.ini
2930
# Docker files
3031
docker-compose.*
3132
Dockerfile
33+
34+
# Test files
35+
tests.py
36+
tests/

.env.defaults

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ DJANGO_DEBUG=True
22
DJANGO_SECRET_KEY=test-key
33
DJANGO_LANGUAGE_CODE=en-us
44
DJANGO_TIME_ZONE=UTC
5+
DJANGO_SETTINGS_MODULE=app.settings.development
56
DJANGO_DATABASE_NAME=djtesting
67
DJANGO_DATABASE_USER=postgres
78
DJANGO_DATABASE_PASSWORD=postgres
@@ -12,6 +13,7 @@ DJANGO_REDIS_URI=DJANGO_REDIS_URI
1213
GROWTH_BOOK_HOST=GROWTH_BOOK_HOST
1314
GROWTH_BOOK_KEY=GROWTH_BOOK_KEY
1415
GROWTH_BOOK_TTL=600
16+
EDGE_HOST=http://localhost:8888
1517
ROLLBAR_TOKEN=ROLLBAR_TOKEN
1618
LOGTAIL_SOURCE_TOKEN=LOGTAIL_SOURCE_TOKEN
1719
DJANGO_Q_NAME=queue-service

.graphql/queries/queries.graphql

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ query ConfigQuery {
22
config {
33
settings {
44
analyticsEnabled
5+
platformSource
56
}
6-
defaultImage {
7+
image {
78
banner,
89
default,
910
error,
@@ -12,13 +13,4 @@ query ConfigQuery {
1213
poster
1314
}
1415
}
15-
}
16-
17-
query HomeQuery {
18-
home {
19-
genres {
20-
image,
21-
mediaId
22-
}
23-
}
2416
}

.graphql/schema.graphql

Lines changed: 27 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,6 @@ schema {
55
mutation: Mutations
66
}
77

8-
"An object with an ID"
9-
interface Node {
10-
"The ID of the object"
11-
id: ID!
12-
}
13-
148
"Airing information about the media"
159
type Airing {
1610
"Season the media was aired"
@@ -35,9 +29,12 @@ type Attribute {
3529

3630
"Client configuration"
3731
type Configuration {
32+
"Genre and media connections"
33+
genres: [Genre]
3834
"Default image resources"
39-
defaultImage: ImageResource
40-
id: Int!
35+
image: ImageResource
36+
"Navigation configurations"
37+
navigation: [Navigation]
4138
"Configuration settings"
4239
settings: Settings
4340
}
@@ -46,43 +43,12 @@ type CreateMediaMutation {
4643
media: Media
4744
}
4845

49-
"Navigation destination properties"
50-
type Destination {
51-
"Navigation destination"
52-
destination: String
53-
id: ID!
54-
navigation: Navigation
55-
"Navigation destination"
56-
type: String
57-
}
58-
5946
"Genre and media ID relation"
6047
type Genre {
61-
"Genre"
62-
genre: String
63-
"Image banners for relation"
64-
image: String
65-
"Media ID"
48+
"Related media ID"
6649
mediaId: Int
67-
}
68-
69-
"Navigation group properties"
70-
type Group {
71-
"Authenticated status"
72-
authenticated: Boolean
73-
"Group internationalization"
74-
i18n: String
75-
id: ID!
76-
"Group ID"
77-
identifier: Int
78-
navigation: Navigation
79-
}
80-
81-
"Home entries"
82-
type Home implements Node {
83-
genres: [Genre]
84-
"The ID of the object"
85-
id: ID!
50+
"Genre title"
51+
name: String
8652
}
8753

8854
"Images for media items"
@@ -99,12 +65,10 @@ type Image {
9965
type ImageResource {
10066
"Banner image URL"
10167
banner: String
102-
config: Configuration
10368
"Default image URL"
10469
default: String
10570
"Error image URL"
10671
error: String
107-
id: ID!
10872
"Info image URL"
10973
info: String
11074
"Loading image URL"
@@ -154,22 +118,31 @@ type Mutations {
154118
createMedia(mediaData: MediaInput!): CreateMediaMutation
155119
}
156120

157-
"Navigation configuration"
121+
"Navigation configuration for an entry"
158122
type Navigation {
159-
destination: Destination
160-
group: Group
161-
"Navigation internationalization"
123+
"Display criteria as semver"
124+
criteria: String
125+
"Target destination"
126+
destination: String
127+
"Associated group for this navigation item"
128+
group: NavigationGroup
129+
"Language resource associated with grouping"
162130
i18n: String
163-
"Navigation icon"
131+
"Image resource associated with the navigation item"
164132
icon: String
165-
id: Int!
133+
}
134+
135+
"Category for a navigation item"
136+
type NavigationGroup {
137+
"Should only display when viewer is authenticated"
138+
authenticated: Boolean
139+
"Language resource associated with grouping"
140+
i18n: String
166141
}
167142

168143
type Query {
169144
"Client configuration"
170145
config: Configuration
171-
"Home entries"
172-
home: Home
173146
"Find a media item by filtering with an id"
174147
media(
175148
"AniDB Id"
@@ -189,15 +162,14 @@ type Query {
189162
"TVDB Id"
190163
tvdb: Int
191164
): Media
192-
navigation: [Navigation]
193165
}
194166

195167
"Client default settings"
196168
type Settings {
197169
"Analytics enabled status"
198170
analyticsEnabled: Boolean
199-
config: Configuration
200-
id: ID!
171+
"Upstream platform for additional services"
172+
platformSource: String
201173
}
202174

203175
"Other source ids of where this media can be found"

app/graphql/schema.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
from graphene import ObjectType
33

44
from config.graphql.queries import ConfigQuery
5-
from home.graphql.queries import HomeQuery
65
from media.graphql.mutations import CreateMediaMutation
76
from media.graphql.queries import MediaQuery
87
from media.graphql.types import MediaObjectType
9-
from navigation.graphql.queries import NavigationQuery
108

119

1210
class Mutations(ObjectType):
@@ -20,7 +18,7 @@ class Subscriptions(ObjectType):
2018
pass
2119

2220

23-
class Query(HomeQuery, ConfigQuery, MediaQuery, NavigationQuery, ObjectType):
21+
class Query(ConfigQuery, MediaQuery, ObjectType):
2422
pass
2523

2624

app/settings/common.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,7 @@ def __get_base_dir() -> str:
6464
"manami",
6565
"xem",
6666
"media",
67-
"home",
6867
"config",
69-
"navigation",
7068
]
7169

7270
MIDDLEWARE = [
@@ -224,3 +222,7 @@ def __get_base_dir() -> str:
224222
'code_version': '1.0',
225223
'root': BASE_DIR,
226224
}
225+
226+
ON_THE_EDGE = {
227+
"host": config("EDGE_HOST", cast=str)
228+
}

config/apps.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
from django.apps import AppConfig
22

3+
from app import container
4+
35

46
class Config(AppConfig):
57
default_auto_field = 'django.db.models.BigAutoField'
68
name = 'config'
9+
verbose_name = 'config'
10+
11+
def ready(self):
12+
super().ready()
13+
container.wire(
14+
modules=[
15+
f"{self.name}.data.repositories",
16+
f"{self.name}.domain.usecases",
17+
]
18+
)

config/data/repositories.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from json import JSONDecodeError
2+
from typing import Optional, Any
3+
4+
from uplink import Consumer
5+
6+
from core.errors import NoDataError
7+
from core.repositories import DataRepository
8+
from .schemas import ConfigurationSchema
9+
from ..data.sources import RemoteSource
10+
from ..domain.entities import (ConfigurationModel, SettingsModel, ImageModel, NavigationModel,
11+
NavigationGroupModel, GenreModel)
12+
from pyxtension.streams import stream
13+
14+
15+
class Repository(DataRepository):
16+
_remote_source: RemoteSource
17+
18+
def __init__(self, remote_source: Consumer) -> None:
19+
super().__init__(remote_source)
20+
21+
def invoke(self, **kwargs) -> ConfigurationModel:
22+
try:
23+
data = self._remote_source.get_config()
24+
return data
25+
except JSONDecodeError as e:
26+
self._logger.error(f"Malformed response with error message `{e.doc}`", exc_info=e)
27+
raise e

config/data/schemas.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from typing import Any
2+
3+
from marshmallow import fields, post_load
4+
5+
from config.domain.entities import ConfigurationModel
6+
from core.schemas import CommonSchema
7+
8+
9+
class SettingsSchema(CommonSchema):
10+
analyticsEnabled = fields.Boolean(required=True)
11+
platformSource = fields.String(allow_none=True)
12+
13+
14+
class ImageSchema(CommonSchema):
15+
banner = fields.String(required=True)
16+
poster = fields.String(required=True)
17+
loading = fields.String(required=True)
18+
error = fields.String(required=True)
19+
info = fields.String(required=True)
20+
default = fields.String(required=True)
21+
22+
23+
class NavigationGroupSchema(CommonSchema):
24+
authenticated = fields.Boolean(required=True)
25+
i18n = fields.String(required=True)
26+
27+
28+
class NavigationSchema(CommonSchema):
29+
criteria = fields.String(required=True)
30+
destination = fields.String(required=True)
31+
i18n = fields.String(required=True)
32+
icon = fields.String(required=True)
33+
group = fields.Nested(NavigationGroupSchema(), required=True)
34+
35+
36+
class GenreSchema(CommonSchema):
37+
name = fields.String(required=True)
38+
mediaId = fields.Integer(required=True)
39+
40+
41+
class ConfigurationSchema(CommonSchema):
42+
settings = fields.Nested(SettingsSchema(), required=True)
43+
image = fields.Nested(ImageSchema(), allow_none=True)
44+
navigation = fields.List(fields.Nested(nested=NavigationSchema(), allow_none=True))
45+
genres = fields.List(fields.Nested(nested=GenreSchema(), allow_none=True))
46+
47+
@post_load()
48+
def __on_post_load(self, data, many, **kwargs) -> ConfigurationModel:
49+
try:
50+
model = ConfigurationModel.from_dict(data)
51+
return model
52+
except Exception as e:
53+
self._logger.error(f"Conversion from dictionary failed", exc_info=e)
54+
raise e

config/data/sources.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from marshmallow import EXCLUDE
2+
from uplink import get, timeout, retry, ratelimit, Consumer
3+
4+
from core.decorators import raise_api_error
5+
from core import __TIME_OUT__, __MAX_ATTEMPTS__, __RATE_LIMIT_CALLS__, __RATE_LIMIT_PERIOD_CALLS__
6+
from ..data.schemas import ConfigurationSchema
7+
8+
9+
@timeout(seconds=__TIME_OUT__)
10+
@retry(
11+
max_attempts=__MAX_ATTEMPTS__,
12+
when=retry.when.raises(Exception),
13+
stop=retry.stop.after_attempt(__MAX_ATTEMPTS__) | retry.stop.after_delay(__RATE_LIMIT_PERIOD_CALLS__),
14+
backoff=retry.backoff.jittered(multiplier=0.5)
15+
)
16+
@ratelimit(
17+
calls=__RATE_LIMIT_CALLS__,
18+
period=__RATE_LIMIT_PERIOD_CALLS__
19+
)
20+
class RemoteSource(Consumer):
21+
22+
@raise_api_error
23+
@get("config")
24+
def get_config(self) -> ConfigurationSchema(unknown=EXCLUDE):
25+
"""
26+
:return: ConfigurationSchema
27+
"""
28+
pass

0 commit comments

Comments
 (0)