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.
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ .pytest_cache/
10
+ .idea/
11
+ .DS_Store
12
+
13
+ # runtime data files (created at app run time)
14
+ reported_items.log
15
+ excluded_items.txt
@@ -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
+ [![CI](https://github.com/filias/memi-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/filias/memi-engine/actions/workflows/ci.yml)
37
+ [![PyPI version](https://img.shields.io/pypi/v/memi-engine.svg)](https://pypi.org/project/memi-engine/)
38
+ [![Python versions](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://pypi.org/project/memi-engine/)
39
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
40
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](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
+ [![CI](https://github.com/filias/memi-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/filias/memi-engine/actions/workflows/ci.yml)
4
+ [![PyPI version](https://img.shields.io/pypi/v/memi-engine.svg)](https://pypi.org/project/memi-engine/)
5
+ [![Python versions](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://pypi.org/project/memi-engine/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](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
+ ]