Coverage for src/models/data.py: 43%

90 statements  

« prev     ^ index     » next       coverage.py v7.3.0, created at 2025-04-20 12:25 +0000

1import collections 

2import json 

3import pathlib 

4import datetime 

5import dataclasses 

6 

7from . import Image 

8 

9 

10def load_data(name, data_dir: str | pathlib.Path): 

11 data_dir = pathlib.Path(data_dir) 

12 data_file = data_dir / f'{name}.json' 

13 with data_file.open('r') as f: 

14 return json.load(f) 

15 

16 

17@dataclasses.dataclass 

18class Game: 

19 """A Game""" 

20 slug: str 

21 """A slugified version of the name to act as an ID""" 

22 

23 description: str 

24 """A short description of the game""" 

25 

26 image: Image 

27 """The game's image (an `Image`)""" 

28 

29 url: str 

30 """A url link to the game download""" 

31 

32 @property 

33 def title(self) -> str: 

34 """A title cased version of the slug for display.""" 

35 return self.slug.title() 

36 

37 

38@dataclasses.dataclass 

39class Spider: 

40 """A Spider""" 

41 personal: str 

42 """The spider's personal name (e.g. *Spidey*)""" 

43 

44 common: str 

45 """The spider's species common name (e.g. *Mexican Rose Grey*)""" 

46 

47 scientific: str 

48 """The spider's species scientific name (e.g. *Tlitiocatl verdezi*)""" 

49 

50 image: Image 

51 """The spider's picture (an `Image`)""" 

52 

53 acquired: datetime.datetime 

54 """The day the spider was acquired""" 

55 

56 deceased: datetime.datetime 

57 """The day the spider died""" 

58 

59 endemic: str 

60 """The spider's natural endemic region (e.g. *Mexico - Southern Guerrero and eastern Oaxaca*)""" 

61 

62 

63def load_games(data_dir: str | pathlib.Path, images=[]) -> list[Game]: 

64 """Load games from the `data_dir` 

65 

66 Games are defined in `data/games.json` in this format: 

67 

68 ```json 

69 [ 

70 { 

71 "description": "fight a bear", 

72 "image": "2025-04-16-bear-fight.png", 

73 "slug": "bear-fight", 

74 "url": "https://www.bear-fight-game.biz" 

75 }, 

76 ] 

77 ``` 

78 

79 This function converts it into a list of `Game` objects. 

80 

81 Pass in site a list of site `Image` objects so the game's image 

82 can be associated. 

83 

84 ```python 

85 games = load_games('./data', images=[]) 

86 ``` 

87 """ 

88 games = [] 

89 

90 for obj in load_data('games', data_dir): 

91 try: 

92 image = next(( 

93 img for img in images if img.filename == obj['image'] 

94 )) 

95 except StopIteration: 

96 raise ValueError(f'could not find game image \"{obj["image"]}\"') 

97 

98 kwargs = obj 

99 kwargs['image'] = image 

100 game = Game(**kwargs) 

101 games.append(game) 

102 

103 return games 

104 

105 

106def load_spiders(data_dir: str | pathlib.Path, images=[]) -> list[Spider]: 

107 """Load spiders from the `data_dir`. 

108 

109 Spiders are defined in `data/spiders.json` in this format: 

110 

111 ```json 

112 [ 

113 { 

114 "acquired": [ 

115 5, 

116 7, 

117 2021 

118 ], 

119 "common": "Mexican Rose Grey", 

120 "deceased": [ 

121 26, 

122 7, 

123 2024 

124 ], 

125 "endemic": "Mexico - Southern Guerrero and eastern Oaxaca", 

126 "image": "2023-06-26-spidey.jpg", 

127 "personal": "Spidey", 

128 "scientific": "Tlitocatl verdezi" 

129 } 

130 ] 

131 ``` 

132 This function reads the same data and converts it into a list of 

133 `Spider` objects in the order they were acquired. 

134 

135 Pass in site a list of site `Image` objects so the spider's image 

136 can be associated. 

137 

138 ```python 

139 spiders = load_spiders('./data') 

140 ``` 

141 """ 

142 spiders = [] 

143 

144 for obj in load_data('spiders', data_dir): 

145 try: 

146 image = next(( 

147 img for img in images if img.filename == obj['image'] 

148 )) 

149 except StopIteration: 

150 raise ValueError(f'could not find spider image \"{obj["image"]}\"') 

151 

152 kwargs = obj 

153 kwargs['image'] = image 

154 day, month, year = kwargs['acquired'] 

155 kwargs['acquired'] = datetime.datetime(year=year, month=month, day=day) 

156 if deceased := kwargs.get('deceased'): 

157 day, month, year = deceased 

158 kwargs['deceased'] = datetime.datetime( 

159 year=year, month=month, day=day) 

160 else: 

161 kwargs['deceased'] = None 

162 spider = Spider(**kwargs) 

163 spiders.append(spider) 

164 

165 spiders.sort(key=lambda s: s.acquired) 

166 return spiders 

167 

168 

169SpiderStats = collections.namedtuple('SpiderStats', [ 

170 'count_living', 

171 'count_deceased', 

172 'oldest_living', 

173 'youngest_living', 

174 'oldest_deceased', 

175 'youngest_deceased', 

176]) 

177 

178 

179def load_spider_stats(spiders: list[Spider]) -> SpiderStats: 

180 """Generate stats from a list of `spiders`. 

181 

182 Returns a `SpiderStats` containing some miscellaneous 

183 stats. 

184 

185 ```python 

186 stats = load_spider_stats(spiders) 

187 ``` 

188 """ 

189 stats = {} 

190 

191 living = [s for s in spiders if not s.deceased] 

192 deceased = [s for s in spiders if s.deceased] 

193 

194 stats['count_living'] = f'{len(living):,}' 

195 stats['count_deceased'] = f'{len(deceased):,}' 

196 

197 today = datetime.datetime.now() 

198 living_by_age = list(sorted(living, key=lambda s: today - s.acquired, reverse=True)) 

199 oldest_living = living_by_age[0] 

200 youngest_living = living_by_age[-1] 

201 stats['oldest_living'] = f'{oldest_living.personal} ({(today - oldest_living.acquired).days:,} days)' 

202 stats['youngest_living'] = f'{youngest_living.personal} ({(today - youngest_living.acquired).days:,} days)' 

203 

204 deceased_by_age = list(sorted(deceased, key=lambda s: s.deceased - s.acquired, reverse=True)) 

205 oldest_deceased = deceased_by_age[0] 

206 stats['oldest_deceased'] = f'{oldest_deceased.personal} ({(oldest_deceased.deceased - oldest_deceased.acquired).days:,} days)' 

207 youngest_deceased = deceased_by_age[-1] 

208 stats['youngest_deceased'] = f'{youngest_deceased.personal} ({(youngest_deceased.deceased - youngest_deceased.acquired).days:,} days)' 

209 return SpiderStats(**stats)