memi-engine 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- memi_engine-0.1.0/.gitignore +15 -0
- memi_engine-0.1.0/LICENSE +21 -0
- memi_engine-0.1.0/PKG-INFO +270 -0
- memi_engine-0.1.0/README.md +237 -0
- memi_engine-0.1.0/memi_engine/__init__.py +29 -0
- memi_engine-0.1.0/memi_engine/app.py +256 -0
- memi_engine-0.1.0/memi_engine/config.py +66 -0
- memi_engine-0.1.0/memi_engine/images.py +579 -0
- memi_engine-0.1.0/memi_engine/menu.py +76 -0
- memi_engine-0.1.0/memi_engine/provider.py +130 -0
- memi_engine-0.1.0/memi_engine/py.typed +0 -0
- memi_engine-0.1.0/memi_engine/registry.py +50 -0
- memi_engine-0.1.0/memi_engine/scientific.py +54 -0
- memi_engine-0.1.0/memi_engine/scientific_names.py +1511 -0
- memi_engine-0.1.0/memi_engine/static/css/style.css +648 -0
- memi_engine-0.1.0/memi_engine/static/js/app.js +420 -0
- memi_engine-0.1.0/memi_engine/templates/about.html +51 -0
- memi_engine-0.1.0/memi_engine/templates/index.html +115 -0
- memi_engine-0.1.0/pyproject.toml +59 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Filipa Andrade
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memi-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Engine for building memi memory card game instances
|
|
5
|
+
Project-URL: Homepage, https://memi.click
|
|
6
|
+
Project-URL: Repository, https://github.com/filias/memi-engine
|
|
7
|
+
Project-URL: Issues, https://github.com/filias/memi-engine/issues
|
|
8
|
+
Author-email: Filipa Andrade <filipa.andrade@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: education,flask,game,memi,memory,quiz
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: Flask
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Education
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Education
|
|
23
|
+
Classifier: Topic :: Games/Entertainment
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: flask>=3.0
|
|
26
|
+
Requires-Dist: requests>=2.31
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
30
|
+
Provides-Extra: server
|
|
31
|
+
Requires-Dist: gunicorn>=22; extra == 'server'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# memi-engine
|
|
35
|
+
|
|
36
|
+
[](https://github.com/filias/memi-engine/actions/workflows/ci.yml)
|
|
37
|
+
[](https://pypi.org/project/memi-engine/)
|
|
38
|
+
[](https://pypi.org/project/memi-engine/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
[](https://github.com/astral-sh/ruff)
|
|
41
|
+
|
|
42
|
+
> In a world where a language model will answer almost anything in an instant, the
|
|
43
|
+
> part of your mind that *recalls* — that retrieves what you know on its own — gets
|
|
44
|
+
> little exercise. **memi** is a small counterweight: a game built on *active
|
|
45
|
+
> recall*. You look at an image, try to name it before revealing the answer, and
|
|
46
|
+
> follow the *know more* link to learn more. Each round strengthens the link
|
|
47
|
+
> between what you see and what you know. The answer is always one tap away — the
|
|
48
|
+
> point is to reach for it yourself first.
|
|
49
|
+
|
|
50
|
+
`memi-engine` lets you build your own [memi](https://memi.click) game — a
|
|
51
|
+
tap-to-reveal flashcard trainer — from a list of names and where to find their
|
|
52
|
+
images.
|
|
53
|
+
|
|
54
|
+
You define **categories** (countries, animals, monuments, movies…); the engine
|
|
55
|
+
gives you the responsive web UI, the menu, image fetching from Wikipedia and
|
|
56
|
+
friends, filters, clue mode, a **"know more" link** to each item's Wikipedia
|
|
57
|
+
(or source) page on reveal, theming, and a reporting system.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install memi-engine
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from memi_engine import CategoryProvider, MemiConfig, create_app, register
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Animals(CategoryProvider):
|
|
68
|
+
key = "nature:animals"
|
|
69
|
+
items = ["Lion", "Tiger", "Elephant", "Aardvark"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
register(Animals())
|
|
73
|
+
app = create_app(MemiConfig(title="My Memi"))
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
app.run(debug=True)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Open <http://localhost:5000>, pick **nature → animals**, and play. Images are
|
|
80
|
+
resolved from Wikipedia automatically from each item's name.
|
|
81
|
+
|
|
82
|
+
## Concepts
|
|
83
|
+
|
|
84
|
+
A memi game is just a set of **category providers** registered with the engine.
|
|
85
|
+
Each provider declares:
|
|
86
|
+
|
|
87
|
+
- **`items`** — the list of names to guess.
|
|
88
|
+
- **`key`** — where the category sits in the menu (see below).
|
|
89
|
+
- **how to get an image** for an item (default: Wikipedia), and optionally a
|
|
90
|
+
**tag** (a subtitle shown on reveal) and a **clue**.
|
|
91
|
+
|
|
92
|
+
The engine handles routing, the random game loop (`/api/random`), filtering,
|
|
93
|
+
prefetching, and rendering.
|
|
94
|
+
|
|
95
|
+
### Keys *are* the menu
|
|
96
|
+
|
|
97
|
+
A category `key` is a colon-separated path. The engine splits it to build a
|
|
98
|
+
nested menu, and **renders each segment verbatim as the on-screen label** — so
|
|
99
|
+
the key is also your menu copy. This is why localized games keep their keys in
|
|
100
|
+
the game's language:
|
|
101
|
+
|
|
102
|
+
| Key | Menu shown to the player |
|
|
103
|
+
| ---------------------------- | ------------------------------- |
|
|
104
|
+
| `"space"` | space |
|
|
105
|
+
| `"nature:animals"` | nature → animals |
|
|
106
|
+
| `"nature:plants:flowers"` | nature → plants → flowers |
|
|
107
|
+
| `"geografia:freguesias"` | geografia → freguesias |
|
|
108
|
+
|
|
109
|
+
Up to four levels are supported. A child labelled `all` always sorts first.
|
|
110
|
+
|
|
111
|
+
## `CategoryProvider`
|
|
112
|
+
|
|
113
|
+
Subclass it and set at least `key` and `items`. Override the methods you need.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
class Monuments(CategoryProvider):
|
|
117
|
+
key = "culture:monuments"
|
|
118
|
+
items = ["Belém Tower", "Eiffel Tower"]
|
|
119
|
+
override_name = True # show the item name, not the article title
|
|
120
|
+
|
|
121
|
+
def get_tag(self, item): # subtitle on the revealed card
|
|
122
|
+
return PARISHES.get(item)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Register each provider with `register(Monuments())`, or use `@register` as a
|
|
126
|
+
class decorator on the definition.
|
|
127
|
+
|
|
128
|
+
**Attributes**
|
|
129
|
+
|
|
130
|
+
| Attribute | Default | Meaning |
|
|
131
|
+
| ---------------- | ------- | ------------------------------------------------------------------ |
|
|
132
|
+
| `key` | `""` | Menu path (see above). |
|
|
133
|
+
| `items` | `[]` | List of item names. |
|
|
134
|
+
| `filters` | `{}` | `{filter_name: {value: [items]}}` — auto-generates filter UI. |
|
|
135
|
+
| `single_select` | `False` | Only one subcategory active at a time. |
|
|
136
|
+
| `light_bg` | `False` | Light card background (good for logos). |
|
|
137
|
+
| `override_name` | `False` | Use the item key as the display name, not the article title. |
|
|
138
|
+
| `footers` | `[]` | Footer IDs (attribution) to show when this category is active. |
|
|
139
|
+
| `tag_style` | `None` | `"plain"`, `"scientific"`, or `None` (auto-detect) — tag styling. |
|
|
140
|
+
|
|
141
|
+
**Methods**
|
|
142
|
+
|
|
143
|
+
| Method | Returns |
|
|
144
|
+
| ---------------------- | ------------------------------------------------------------- |
|
|
145
|
+
| `get_image(item)` | `{"name": ..., "image": ..., "url": ...}` or `None`. Default: Wikipedia. |
|
|
146
|
+
| `get_tag(item)` | A short subtitle for the revealed card, or `None`. |
|
|
147
|
+
| `get_clue(item)` | A clue shown *before* reveal, or `None`. |
|
|
148
|
+
|
|
149
|
+
The optional **`url`** in the `get_image` result is the item's source page; the
|
|
150
|
+
engine turns it into the *"know more"* link shown on reveal (label set via
|
|
151
|
+
`MemiConfig.label_more`). The built-in image helpers populate it automatically —
|
|
152
|
+
e.g. `get_wikipedia_image` returns the Wikipedia article URL — so Wikipedia-backed
|
|
153
|
+
categories get the link for free.
|
|
154
|
+
|
|
155
|
+
### Scientific names
|
|
156
|
+
|
|
157
|
+
`ScientificNameProvider` tags each item with its Latin name. It ships a bundled
|
|
158
|
+
English database (`SCIENTIFIC_NAMES`, ~1500 species) used by default; pass your
|
|
159
|
+
own mapping for other languages. The tag is shown only when the Latin name
|
|
160
|
+
differs from the display name, in italic *scientific* style.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from memi_engine import ScientificNameProvider, register
|
|
164
|
+
|
|
165
|
+
class Animals(ScientificNameProvider):
|
|
166
|
+
key = "nature:animals"
|
|
167
|
+
items = ["Lion", "Tiger"] # → "Panthera leo", "Panthera tigris"
|
|
168
|
+
|
|
169
|
+
class Plantas(ScientificNameProvider):
|
|
170
|
+
key = "natureza:plantas"
|
|
171
|
+
items = ["Sobreiro"]
|
|
172
|
+
scientific_names = {"Sobreiro": "Quercus suber"}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Filters
|
|
176
|
+
|
|
177
|
+
A filter maps option values to subsets of `items`. The engine renders the filter
|
|
178
|
+
buttons and applies the choice via a URL parameter.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
class Countries(CategoryProvider):
|
|
182
|
+
key = "geography:countries"
|
|
183
|
+
items = ["France", "Spain", "Japan"]
|
|
184
|
+
filters = {
|
|
185
|
+
"continent": {"europe": ["France", "Spain"], "asia": ["Japan"]},
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Images
|
|
190
|
+
|
|
191
|
+
`memi_engine.images` resolves item names to image URLs and caches results
|
|
192
|
+
in-memory. Providers call these from `get_image`:
|
|
193
|
+
|
|
194
|
+
`get_wikipedia_image`, `get_wikipedia_file_image`, `get_commons_file_image`,
|
|
195
|
+
`get_tmdb_image`, `get_tmdb_tv_image`, `get_fandom_image`, `get_country_shape`,
|
|
196
|
+
`get_album_cover`, `get_logo_image`, and more.
|
|
197
|
+
|
|
198
|
+
Some sources need configuration via environment variables:
|
|
199
|
+
|
|
200
|
+
| Variable | Used by | Default |
|
|
201
|
+
| --------------- | ------------------------------- | ------------------------ |
|
|
202
|
+
| `TMDB_API_KEY` | `get_tmdb_image` (movies / TV) | _(unset — TMDB skipped)_ |
|
|
203
|
+
| `BONES_API_URL` | anatomy image service | `http://127.0.0.1:8081` |
|
|
204
|
+
|
|
205
|
+
## `MemiConfig`
|
|
206
|
+
|
|
207
|
+
Passed to `create_app`. Common fields:
|
|
208
|
+
|
|
209
|
+
| Field | Default | Purpose |
|
|
210
|
+
| ----------------- | -------------------- | ---------------------------------------- |
|
|
211
|
+
| `title` | `"memi"` | Header title. |
|
|
212
|
+
| `subtitle` | `"practise…"` | Header subtitle. |
|
|
213
|
+
| `themes` | 8 built-in themes | Available colour themes. |
|
|
214
|
+
| `default_theme` | `"light"` | Initial theme. |
|
|
215
|
+
| `sponsor_url` | `None` | Sponsor link (hidden if `None`). |
|
|
216
|
+
| `about_html` | `None` | Custom HTML for the about page. |
|
|
217
|
+
| `analytics_html` | `None` | Analytics snippet injected on the page. |
|
|
218
|
+
| `favicon_color` | `"#b8860b"` | Favicon background colour. |
|
|
219
|
+
| `wikipedia_lang` | `"en"` | Wikipedia edition for default images / *know more* links. |
|
|
220
|
+
| `related_sites` | `[]` | Sibling games to link from the about page. |
|
|
221
|
+
| `label_*` | English strings | UI labels (for localization). |
|
|
222
|
+
|
|
223
|
+
For a non-English game, set `wikipedia_lang` so the default image lookup and the
|
|
224
|
+
*"know more"* link resolve against that language's Wikipedia (e.g. `"pt"`). It
|
|
225
|
+
can also be set with the `MEMI_WIKIPEDIA_LANG` environment variable.
|
|
226
|
+
|
|
227
|
+
All UI strings are `label_*` fields, so a fully localized game keeps its labels
|
|
228
|
+
and `about_html` in its own language while the code stays English.
|
|
229
|
+
|
|
230
|
+
### Instance static files
|
|
231
|
+
|
|
232
|
+
To serve your own logo or images, point `create_app` at a static folder; its
|
|
233
|
+
files take precedence over the engine's:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
app = create_app(config, instance_static="/path/to/static")
|
|
237
|
+
# served at /static/... , falling back to the engine's static files
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Deployment
|
|
241
|
+
|
|
242
|
+
The app is a standard WSGI Flask app. For production, install the `server`
|
|
243
|
+
extra and run under gunicorn:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
pip install "memi-engine[server]"
|
|
247
|
+
gunicorn "yourgame:app"
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Two optional data files are read from the working directory at runtime:
|
|
251
|
+
`excluded_items.txt` (items to hide) and `reported_items.log` (written when
|
|
252
|
+
players report a bad card).
|
|
253
|
+
|
|
254
|
+
## Live examples
|
|
255
|
+
|
|
256
|
+
Real games built on this engine: [memi](https://memi.click) ·
|
|
257
|
+
[memi portugal](https://pt.memi.click) · [memi lisboa](https://lx.memi.click) ·
|
|
258
|
+
[memi slovensko](https://sk.memi.click) · [memi US](https://us.memi.click).
|
|
259
|
+
|
|
260
|
+
## Development
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
uv sync --extra dev
|
|
264
|
+
pytest # run the test suite
|
|
265
|
+
ruff check . # lint
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# memi-engine
|
|
2
|
+
|
|
3
|
+
[](https://github.com/filias/memi-engine/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/memi-engine/)
|
|
5
|
+
[](https://pypi.org/project/memi-engine/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://github.com/astral-sh/ruff)
|
|
8
|
+
|
|
9
|
+
> In a world where a language model will answer almost anything in an instant, the
|
|
10
|
+
> part of your mind that *recalls* — that retrieves what you know on its own — gets
|
|
11
|
+
> little exercise. **memi** is a small counterweight: a game built on *active
|
|
12
|
+
> recall*. You look at an image, try to name it before revealing the answer, and
|
|
13
|
+
> follow the *know more* link to learn more. Each round strengthens the link
|
|
14
|
+
> between what you see and what you know. The answer is always one tap away — the
|
|
15
|
+
> point is to reach for it yourself first.
|
|
16
|
+
|
|
17
|
+
`memi-engine` lets you build your own [memi](https://memi.click) game — a
|
|
18
|
+
tap-to-reveal flashcard trainer — from a list of names and where to find their
|
|
19
|
+
images.
|
|
20
|
+
|
|
21
|
+
You define **categories** (countries, animals, monuments, movies…); the engine
|
|
22
|
+
gives you the responsive web UI, the menu, image fetching from Wikipedia and
|
|
23
|
+
friends, filters, clue mode, a **"know more" link** to each item's Wikipedia
|
|
24
|
+
(or source) page on reveal, theming, and a reporting system.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install memi-engine
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from memi_engine import CategoryProvider, MemiConfig, create_app, register
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Animals(CategoryProvider):
|
|
35
|
+
key = "nature:animals"
|
|
36
|
+
items = ["Lion", "Tiger", "Elephant", "Aardvark"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
register(Animals())
|
|
40
|
+
app = create_app(MemiConfig(title="My Memi"))
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
app.run(debug=True)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Open <http://localhost:5000>, pick **nature → animals**, and play. Images are
|
|
47
|
+
resolved from Wikipedia automatically from each item's name.
|
|
48
|
+
|
|
49
|
+
## Concepts
|
|
50
|
+
|
|
51
|
+
A memi game is just a set of **category providers** registered with the engine.
|
|
52
|
+
Each provider declares:
|
|
53
|
+
|
|
54
|
+
- **`items`** — the list of names to guess.
|
|
55
|
+
- **`key`** — where the category sits in the menu (see below).
|
|
56
|
+
- **how to get an image** for an item (default: Wikipedia), and optionally a
|
|
57
|
+
**tag** (a subtitle shown on reveal) and a **clue**.
|
|
58
|
+
|
|
59
|
+
The engine handles routing, the random game loop (`/api/random`), filtering,
|
|
60
|
+
prefetching, and rendering.
|
|
61
|
+
|
|
62
|
+
### Keys *are* the menu
|
|
63
|
+
|
|
64
|
+
A category `key` is a colon-separated path. The engine splits it to build a
|
|
65
|
+
nested menu, and **renders each segment verbatim as the on-screen label** — so
|
|
66
|
+
the key is also your menu copy. This is why localized games keep their keys in
|
|
67
|
+
the game's language:
|
|
68
|
+
|
|
69
|
+
| Key | Menu shown to the player |
|
|
70
|
+
| ---------------------------- | ------------------------------- |
|
|
71
|
+
| `"space"` | space |
|
|
72
|
+
| `"nature:animals"` | nature → animals |
|
|
73
|
+
| `"nature:plants:flowers"` | nature → plants → flowers |
|
|
74
|
+
| `"geografia:freguesias"` | geografia → freguesias |
|
|
75
|
+
|
|
76
|
+
Up to four levels are supported. A child labelled `all` always sorts first.
|
|
77
|
+
|
|
78
|
+
## `CategoryProvider`
|
|
79
|
+
|
|
80
|
+
Subclass it and set at least `key` and `items`. Override the methods you need.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
class Monuments(CategoryProvider):
|
|
84
|
+
key = "culture:monuments"
|
|
85
|
+
items = ["Belém Tower", "Eiffel Tower"]
|
|
86
|
+
override_name = True # show the item name, not the article title
|
|
87
|
+
|
|
88
|
+
def get_tag(self, item): # subtitle on the revealed card
|
|
89
|
+
return PARISHES.get(item)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Register each provider with `register(Monuments())`, or use `@register` as a
|
|
93
|
+
class decorator on the definition.
|
|
94
|
+
|
|
95
|
+
**Attributes**
|
|
96
|
+
|
|
97
|
+
| Attribute | Default | Meaning |
|
|
98
|
+
| ---------------- | ------- | ------------------------------------------------------------------ |
|
|
99
|
+
| `key` | `""` | Menu path (see above). |
|
|
100
|
+
| `items` | `[]` | List of item names. |
|
|
101
|
+
| `filters` | `{}` | `{filter_name: {value: [items]}}` — auto-generates filter UI. |
|
|
102
|
+
| `single_select` | `False` | Only one subcategory active at a time. |
|
|
103
|
+
| `light_bg` | `False` | Light card background (good for logos). |
|
|
104
|
+
| `override_name` | `False` | Use the item key as the display name, not the article title. |
|
|
105
|
+
| `footers` | `[]` | Footer IDs (attribution) to show when this category is active. |
|
|
106
|
+
| `tag_style` | `None` | `"plain"`, `"scientific"`, or `None` (auto-detect) — tag styling. |
|
|
107
|
+
|
|
108
|
+
**Methods**
|
|
109
|
+
|
|
110
|
+
| Method | Returns |
|
|
111
|
+
| ---------------------- | ------------------------------------------------------------- |
|
|
112
|
+
| `get_image(item)` | `{"name": ..., "image": ..., "url": ...}` or `None`. Default: Wikipedia. |
|
|
113
|
+
| `get_tag(item)` | A short subtitle for the revealed card, or `None`. |
|
|
114
|
+
| `get_clue(item)` | A clue shown *before* reveal, or `None`. |
|
|
115
|
+
|
|
116
|
+
The optional **`url`** in the `get_image` result is the item's source page; the
|
|
117
|
+
engine turns it into the *"know more"* link shown on reveal (label set via
|
|
118
|
+
`MemiConfig.label_more`). The built-in image helpers populate it automatically —
|
|
119
|
+
e.g. `get_wikipedia_image` returns the Wikipedia article URL — so Wikipedia-backed
|
|
120
|
+
categories get the link for free.
|
|
121
|
+
|
|
122
|
+
### Scientific names
|
|
123
|
+
|
|
124
|
+
`ScientificNameProvider` tags each item with its Latin name. It ships a bundled
|
|
125
|
+
English database (`SCIENTIFIC_NAMES`, ~1500 species) used by default; pass your
|
|
126
|
+
own mapping for other languages. The tag is shown only when the Latin name
|
|
127
|
+
differs from the display name, in italic *scientific* style.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from memi_engine import ScientificNameProvider, register
|
|
131
|
+
|
|
132
|
+
class Animals(ScientificNameProvider):
|
|
133
|
+
key = "nature:animals"
|
|
134
|
+
items = ["Lion", "Tiger"] # → "Panthera leo", "Panthera tigris"
|
|
135
|
+
|
|
136
|
+
class Plantas(ScientificNameProvider):
|
|
137
|
+
key = "natureza:plantas"
|
|
138
|
+
items = ["Sobreiro"]
|
|
139
|
+
scientific_names = {"Sobreiro": "Quercus suber"}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Filters
|
|
143
|
+
|
|
144
|
+
A filter maps option values to subsets of `items`. The engine renders the filter
|
|
145
|
+
buttons and applies the choice via a URL parameter.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
class Countries(CategoryProvider):
|
|
149
|
+
key = "geography:countries"
|
|
150
|
+
items = ["France", "Spain", "Japan"]
|
|
151
|
+
filters = {
|
|
152
|
+
"continent": {"europe": ["France", "Spain"], "asia": ["Japan"]},
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Images
|
|
157
|
+
|
|
158
|
+
`memi_engine.images` resolves item names to image URLs and caches results
|
|
159
|
+
in-memory. Providers call these from `get_image`:
|
|
160
|
+
|
|
161
|
+
`get_wikipedia_image`, `get_wikipedia_file_image`, `get_commons_file_image`,
|
|
162
|
+
`get_tmdb_image`, `get_tmdb_tv_image`, `get_fandom_image`, `get_country_shape`,
|
|
163
|
+
`get_album_cover`, `get_logo_image`, and more.
|
|
164
|
+
|
|
165
|
+
Some sources need configuration via environment variables:
|
|
166
|
+
|
|
167
|
+
| Variable | Used by | Default |
|
|
168
|
+
| --------------- | ------------------------------- | ------------------------ |
|
|
169
|
+
| `TMDB_API_KEY` | `get_tmdb_image` (movies / TV) | _(unset — TMDB skipped)_ |
|
|
170
|
+
| `BONES_API_URL` | anatomy image service | `http://127.0.0.1:8081` |
|
|
171
|
+
|
|
172
|
+
## `MemiConfig`
|
|
173
|
+
|
|
174
|
+
Passed to `create_app`. Common fields:
|
|
175
|
+
|
|
176
|
+
| Field | Default | Purpose |
|
|
177
|
+
| ----------------- | -------------------- | ---------------------------------------- |
|
|
178
|
+
| `title` | `"memi"` | Header title. |
|
|
179
|
+
| `subtitle` | `"practise…"` | Header subtitle. |
|
|
180
|
+
| `themes` | 8 built-in themes | Available colour themes. |
|
|
181
|
+
| `default_theme` | `"light"` | Initial theme. |
|
|
182
|
+
| `sponsor_url` | `None` | Sponsor link (hidden if `None`). |
|
|
183
|
+
| `about_html` | `None` | Custom HTML for the about page. |
|
|
184
|
+
| `analytics_html` | `None` | Analytics snippet injected on the page. |
|
|
185
|
+
| `favicon_color` | `"#b8860b"` | Favicon background colour. |
|
|
186
|
+
| `wikipedia_lang` | `"en"` | Wikipedia edition for default images / *know more* links. |
|
|
187
|
+
| `related_sites` | `[]` | Sibling games to link from the about page. |
|
|
188
|
+
| `label_*` | English strings | UI labels (for localization). |
|
|
189
|
+
|
|
190
|
+
For a non-English game, set `wikipedia_lang` so the default image lookup and the
|
|
191
|
+
*"know more"* link resolve against that language's Wikipedia (e.g. `"pt"`). It
|
|
192
|
+
can also be set with the `MEMI_WIKIPEDIA_LANG` environment variable.
|
|
193
|
+
|
|
194
|
+
All UI strings are `label_*` fields, so a fully localized game keeps its labels
|
|
195
|
+
and `about_html` in its own language while the code stays English.
|
|
196
|
+
|
|
197
|
+
### Instance static files
|
|
198
|
+
|
|
199
|
+
To serve your own logo or images, point `create_app` at a static folder; its
|
|
200
|
+
files take precedence over the engine's:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
app = create_app(config, instance_static="/path/to/static")
|
|
204
|
+
# served at /static/... , falling back to the engine's static files
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Deployment
|
|
208
|
+
|
|
209
|
+
The app is a standard WSGI Flask app. For production, install the `server`
|
|
210
|
+
extra and run under gunicorn:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
pip install "memi-engine[server]"
|
|
214
|
+
gunicorn "yourgame:app"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Two optional data files are read from the working directory at runtime:
|
|
218
|
+
`excluded_items.txt` (items to hide) and `reported_items.log` (written when
|
|
219
|
+
players report a bad card).
|
|
220
|
+
|
|
221
|
+
## Live examples
|
|
222
|
+
|
|
223
|
+
Real games built on this engine: [memi](https://memi.click) ·
|
|
224
|
+
[memi portugal](https://pt.memi.click) · [memi lisboa](https://lx.memi.click) ·
|
|
225
|
+
[memi slovensko](https://sk.memi.click) · [memi US](https://us.memi.click).
|
|
226
|
+
|
|
227
|
+
## Development
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
uv sync --extra dev
|
|
231
|
+
pytest # run the test suite
|
|
232
|
+
ruff check . # lint
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""memi-engine: build your own memi memory card game.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from memi_engine import CategoryProvider, MemiConfig, create_app, register
|
|
5
|
+
|
|
6
|
+
class MyCategory(CategoryProvider):
|
|
7
|
+
key = "my:category"
|
|
8
|
+
items = ["Item 1", "Item 2"]
|
|
9
|
+
|
|
10
|
+
register(MyCategory())
|
|
11
|
+
|
|
12
|
+
app = create_app(MemiConfig(title="My Memi"))
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from memi_engine.app import create_app
|
|
16
|
+
from memi_engine.config import MemiConfig
|
|
17
|
+
from memi_engine.provider import AggregateProvider, CategoryProvider
|
|
18
|
+
from memi_engine.registry import register
|
|
19
|
+
from memi_engine.scientific import SCIENTIFIC_NAMES, ScientificNameProvider
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"SCIENTIFIC_NAMES",
|
|
23
|
+
"AggregateProvider",
|
|
24
|
+
"CategoryProvider",
|
|
25
|
+
"MemiConfig",
|
|
26
|
+
"ScientificNameProvider",
|
|
27
|
+
"create_app",
|
|
28
|
+
"register",
|
|
29
|
+
]
|