Coverage for app/backend/src/couchers/servicers/resources.py: 100%

35 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-03 11:56 +0000

1import functools 

2import logging 

3from collections import Counter 

4 

5import babel 

6from google.protobuf import empty_pb2 

7from sqlalchemy.orm import Session 

8 

9from couchers.context import CouchersContext 

10from couchers.i18n.locales import get_babel_locale, get_main_i18next 

11from couchers.i18n.localize import try_localize_language_name_from_iso639, try_localize_region_name_from_iso3166 

12from couchers.proto import resources_pb2, resources_pb2_grpc 

13from couchers.resources import get_badge_dict, get_icon, get_language_dict, get_region_dict, get_terms_of_service 

14 

15logger = logging.getLogger(__name__) 

16 

17COMMUNITY_GUIDELINES = [ 

18 {"key": "be_kind", "icon": "hand_with_heart.svg"}, 

19 {"key": "no_dating", "icon": "ghost.svg"}, 

20 {"key": "be_safe", "icon": "shield.svg"}, 

21 {"key": "no_money", "icon": "handshake.svg"}, 

22] 

23 

24 

25# These responses are static per locale, and localizing ~300 names costs tens of ms of CPU (mostly 

26# babel.Locale.parse), so cache the built protos. Sharing one message across requests is safe as 

27# long as nothing mutates responses after return (the tracing interceptor deepcopies before 

28# sanitizing). 

29@functools.lru_cache 

30def _get_regions_res(locale: babel.Locale) -> resources_pb2.GetRegionsRes: 

31 return resources_pb2.GetRegionsRes( 

32 regions=[ 

33 resources_pb2.Region( 

34 alpha3=alpha3, 

35 name=try_localize_region_name_from_iso3166(alpha3, [locale]) or english_name, 

36 ) 

37 for alpha3, english_name in sorted(get_region_dict().items()) 

38 ] 

39 ) 

40 

41 

42@functools.lru_cache 

43def _get_languages_res(locale_list: tuple[str, ...]) -> resources_pb2.GetLanguagesRes: 

44 locale = get_babel_locale(locale_list[0]) 

45 names_by_code = { 

46 code: try_localize_language_name_from_iso639(code, [locale], standalone=True) or english_name 

47 for code, english_name in sorted(get_language_dict().items()) 

48 } 

49 name_counts = Counter(names_by_code.values()) 

50 i18next = get_main_i18next() 

51 display_names_by_code = { 

52 code: ( 

53 i18next.localize( 

54 "ambiguous_language_display_format", 

55 list(locale_list), 

56 substitutions={"name": name, "code": code}, 

57 ) 

58 if name_counts[name] > 1 

59 else name 

60 ) 

61 for code, name in names_by_code.items() 

62 } 

63 return resources_pb2.GetLanguagesRes( 

64 languages=[resources_pb2.Language(code=code, name=name) for code, name in display_names_by_code.items()] 

65 ) 

66 

67 

68class Resources(resources_pb2_grpc.ResourcesServicer): 

69 def GetTermsOfService( 

70 self, request: empty_pb2.Empty, context: CouchersContext, session: Session 

71 ) -> resources_pb2.GetTermsOfServiceRes: 

72 return resources_pb2.GetTermsOfServiceRes(terms_of_service=get_terms_of_service()) 

73 

74 def GetCommunityGuidelines( 

75 self, request: empty_pb2.Empty, context: CouchersContext, session: Session 

76 ) -> resources_pb2.GetCommunityGuidelinesRes: 

77 return resources_pb2.GetCommunityGuidelinesRes( 

78 community_guidelines=[ 

79 resources_pb2.CommunityGuideline( 

80 title=context.localization.localize_string(f"community_guidelines.{cg['key']}_title"), 

81 guideline=context.localization.localize_string(f"community_guidelines.{cg['key']}_guideline"), 

82 icon_svg=get_icon(cg["icon"]), 

83 ) 

84 for cg in COMMUNITY_GUIDELINES 

85 ] 

86 ) 

87 

88 def GetRegions( 

89 self, request: empty_pb2.Empty, context: CouchersContext, session: Session 

90 ) -> resources_pb2.GetRegionsRes: 

91 return _get_regions_res(context.localization.preferred_babel_locale) 

92 

93 def GetLanguages( 

94 self, request: empty_pb2.Empty, context: CouchersContext, session: Session 

95 ) -> resources_pb2.GetLanguagesRes: 

96 return _get_languages_res(tuple(context.localization.locale_list)) 

97 

98 def GetBadges( 

99 self, request: empty_pb2.Empty, context: CouchersContext, session: Session 

100 ) -> resources_pb2.GetBadgesRes: 

101 return resources_pb2.GetBadgesRes( 

102 badges=[ 

103 resources_pb2.Badge( 

104 id=badge.id, 

105 name=context.localization.localize_string(f"badges.{badge.id}_name"), 

106 description=context.localization.localize_string(f"badges.{badge.id}_description"), 

107 color=badge.color, 

108 ) 

109 for badge in get_badge_dict().values() 

110 ] 

111 )