career-emulator-engine 1.0.1__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,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: career_emulator_engine
3
+ Version: 1.0.1
4
+ Summary: TechArena English track: career simulation engine (CLI + MCP). PyPI: career-emulator-engine.
5
+ Author-email: Yuwei <yuwei.willow.z@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/RAI-Internal/techarena-career-sim
8
+ Project-URL: Documentation, https://career-emulator.readthedocs.io
9
+ Project-URL: Source, https://github.com/RAI-Internal/career-emulator-engine
10
+ Project-URL: Issues, https://github.com/RAI-Internal/techarena-career-sim/issues
11
+ Keywords: python,career-sim,simulation,agent,llm,techarena,competition,mcp client,client,mcp,fastmcp,mcp server,model context protocol
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: Implementation :: CPython
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Intended Audience :: Developers
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: <4,>=3.11
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: msgpack>=1.0.0
27
+ Requires-Dist: aiosqlite>=0.22.1
28
+ Requires-Dist: aiofiles>=25.1.0
29
+ Requires-Dist: career-emulator-techarena==1.0.*
30
+ Provides-Extra: mcp
31
+ Requires-Dist: fastmcp>=2; extra == "mcp"
32
+
33
+ # career-emulator-engine
34
+
35
+ English **TechArena** simulation engine (CLI + MCP).
36
+ Separate from [`career-emulator`](https://pypi.org/project/career-emulator/) (BDCI / Chinese) and from the dataset [`career-emulator-techarena`](https://pypi.org/project/career-emulator-techarena/).
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install "career-emulator-engine[mcp]"
42
+ ```
43
+
44
+ ## Quick use
45
+
46
+ ```bash
47
+ career-emulator-engine update --source distribution --split dev
48
+ career-emulator-engine play
49
+ career-emulator-engine-mcp --help
50
+ ```
51
+
52
+ Participant starter: [techarena-career-sim](https://github.com/RAI-Internal/techarena-career-sim)
@@ -0,0 +1,20 @@
1
+ # career-emulator-engine
2
+
3
+ English **TechArena** simulation engine (CLI + MCP).
4
+ Separate from [`career-emulator`](https://pypi.org/project/career-emulator/) (BDCI / Chinese) and from the dataset [`career-emulator-techarena`](https://pypi.org/project/career-emulator-techarena/).
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pip install "career-emulator-engine[mcp]"
10
+ ```
11
+
12
+ ## Quick use
13
+
14
+ ```bash
15
+ career-emulator-engine update --source distribution --split dev
16
+ career-emulator-engine play
17
+ career-emulator-engine-mcp --help
18
+ ```
19
+
20
+ Participant starter: [techarena-career-sim](https://github.com/RAI-Internal/techarena-career-sim)
@@ -0,0 +1,359 @@
1
+ """SQLite persistence for Career Emulator sessions and logs."""
2
+
3
+ import asyncio
4
+ import importlib.util
5
+ import json
6
+ import logging
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ from collections.abc import Callable
11
+ from importlib.metadata import version
12
+ from pathlib import Path
13
+ from typing import Literal, cast
14
+
15
+ import aiosqlite
16
+
17
+ from career_emulator.pretty_print import _red
18
+ from career_emulator.server.models import GameSession, LogEntry
19
+
20
+ DatasetSplit = Literal["dev", "dev_final", "test", "test_final", "*"]
21
+ DatasetSource = Literal["distribution", "github"]
22
+
23
+ DATASET_REPO_URL = "https://github.com/Flash-Tomato/TomatoEmuData-Test"
24
+ DATA_DIR = Path(__file__).with_name("data")
25
+ DATASET_DIR = DATA_DIR / "dataset"
26
+ DATASET_SPLIT: DatasetSplit = cast(DatasetSplit, os.environ.get("CAREER_EMULATOR_DATASET", "dev"))
27
+ DEFAULT_DATASET_SOURCE: DatasetSource = cast(DatasetSource, os.getenv("CAREER_EMULATOR_DATASET_SOURCE") or "distribution")
28
+ DISTRIBUTION_PACKAGE_MODULE = "career_emulator_techarena"
29
+ DEFAULT_DB_PATH = Path(os.environ.get("CAREER_EMULATOR_DB", DATA_DIR / ".career_emulator.sqlite3"))
30
+ DEFAULT_LOG_DIR = Path(os.environ.get("CAREER_EMULATOR_LOG_DIR", DATA_DIR / "logs"))
31
+ MAX_NOTE_LENGTH = max(1, int(os.environ.get("CAREER_EMULATOR_MAX_NOTE_LENGTH", "1000")))
32
+ MAX_SESSIONS = max(1, int(os.environ.get("CAREER_EMULATOR_MAX_SESSIONS", "20")))
33
+
34
+ _session_loggers: dict[str, logging.Logger] = {}
35
+
36
+
37
+ def truncate_note(note: str, max_length: int = MAX_NOTE_LENGTH) -> str:
38
+ """Return a note trimmed to the configured maximum length.
39
+
40
+ :param note: Raw agent note text.
41
+ :param max_length: Maximum allowed note length.
42
+ :returns: Trimmed note text.
43
+ """
44
+ compact = note.strip()
45
+ if len(compact) <= max_length:
46
+ return compact
47
+ return f"{compact[: max_length - 3]}..."
48
+
49
+
50
+ def _normalize_git_remote_url(url: str) -> str:
51
+ """Return a canonical form for comparing git remote URLs."""
52
+ return url.strip().removesuffix(".git").rstrip("/")
53
+
54
+
55
+ def _dataset_origin_matches(expected_url: str, dataset_dir: Path) -> bool:
56
+ """Return whether ``origin`` points at the expected dataset repository."""
57
+ result = subprocess.run(
58
+ ["git", "-C", str(dataset_dir), "remote", "get-url", "origin"],
59
+ check=True,
60
+ capture_output=True,
61
+ text=True,
62
+ )
63
+ origin_url = result.stdout.strip()
64
+ return _normalize_git_remote_url(origin_url) == _normalize_git_remote_url(expected_url)
65
+
66
+
67
+ def _resolve_distribution_dataset() -> Path:
68
+ """Locate the dataset directory from ``career_emulator_techarena``.
69
+
70
+ :returns: Absolute path to the split-specific dataset directory inside the package.
71
+ :raises RuntimeError: If the package is not installed or the split directory is missing.
72
+ """
73
+ module_name = DISTRIBUTION_PACKAGE_MODULE
74
+ spec = importlib.util.find_spec(module_name)
75
+ if spec is None or spec.origin is None:
76
+ msg = (
77
+ "Package 'career-emulator-techarena' is not installed. "
78
+ "Install it with: pip install career-emulator-engine"
79
+ )
80
+ raise RuntimeError(_red(msg))
81
+
82
+ package_root = Path(spec.origin).parent
83
+ split_dataset = package_root / DATASET_SPLIT / "dataset"
84
+ if not split_dataset.is_dir():
85
+ pkg_ver = version(module_name)
86
+ msg = f"Dataset split '{DATASET_SPLIT}' not found in {module_name} ({pkg_ver}) at {split_dataset}"
87
+ raise RuntimeError(_red(msg))
88
+ return split_dataset.resolve()
89
+
90
+
91
+ def update_dataset(source: Literal["github", "distribution"] = DEFAULT_DATASET_SOURCE) -> Path:
92
+ """Clone, force-update, or symlink the Career Emulator event dataset.
93
+
94
+ When source is ``"github"``, the dataset is cloned/updated from the remote
95
+ repository. When ``"distribution"``, a symlink is created pointing to the
96
+ dataset directory bundled inside ``career_emulator_techarena`` for the active
97
+ :data:`DATASET_SPLIT`.
98
+
99
+ :param source: Where to obtain the dataset from.
100
+ :returns: Absolute path to the local dataset directory.
101
+ """
102
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
103
+ absolute_dataset_dir = DATASET_DIR.absolute()
104
+
105
+ if source == "distribution":
106
+ pkg_dataset = _resolve_distribution_dataset()
107
+ if absolute_dataset_dir.is_symlink():
108
+ if absolute_dataset_dir.resolve() == pkg_dataset:
109
+ return absolute_dataset_dir
110
+ absolute_dataset_dir.unlink()
111
+ elif absolute_dataset_dir.is_dir():
112
+ shutil.rmtree(absolute_dataset_dir)
113
+ absolute_dataset_dir.symlink_to(pkg_dataset, target_is_directory=True)
114
+ return absolute_dataset_dir
115
+
116
+ if absolute_dataset_dir.is_symlink():
117
+ absolute_dataset_dir.unlink()
118
+
119
+ if not absolute_dataset_dir.exists():
120
+ subprocess.run(
121
+ ["git", "clone", DATASET_REPO_URL, str(absolute_dataset_dir)],
122
+ check=True,
123
+ )
124
+ return absolute_dataset_dir
125
+
126
+ if not (absolute_dataset_dir / ".git").exists():
127
+ msg = f"Dataset path exists but is not a git checkout: {absolute_dataset_dir}"
128
+ raise RuntimeError(_red(msg))
129
+
130
+ if not _dataset_origin_matches(DATASET_REPO_URL, absolute_dataset_dir):
131
+ shutil.rmtree(absolute_dataset_dir)
132
+ return update_dataset()
133
+
134
+ subprocess.run(["git", "-C", str(absolute_dataset_dir), "fetch", "origin"], check=True)
135
+ subprocess.run(["git", "-C", str(absolute_dataset_dir), "reset", "--hard", "origin/main"], check=True)
136
+ subprocess.run(["git", "-C", str(absolute_dataset_dir), "clean", "-fd"], check=True)
137
+ return absolute_dataset_dir
138
+
139
+
140
+ def _close_session_logger(session_id: str) -> None:
141
+ """Close and remove a cached session logger."""
142
+ logger = _session_loggers.pop(session_id, None)
143
+ if logger is None:
144
+ return
145
+ for handler in logger.handlers[:]:
146
+ handler.close()
147
+ logger.removeHandler(handler)
148
+
149
+
150
+ def _session_logger(session_id: str, log_path: Path) -> logging.Logger:
151
+ """Return a file logger for one session log file."""
152
+ cached = _session_loggers.get(session_id)
153
+ if cached is not None:
154
+ return cached
155
+
156
+ log_path.parent.mkdir(parents=True, exist_ok=True)
157
+ logger = logging.getLogger(f"career_emulator.session.{session_id}")
158
+ logger.setLevel(logging.INFO)
159
+ logger.propagate = False
160
+ handler = logging.FileHandler(log_path, encoding="utf-8")
161
+ handler.setFormatter(logging.Formatter("%(message)s"))
162
+ logger.addHandler(handler)
163
+ _session_loggers[session_id] = logger
164
+ return logger
165
+
166
+
167
+ class SessionStore:
168
+ """Persist game sessions and player logs in SQLite."""
169
+
170
+ def __init__(self, db_path: Path = DEFAULT_DB_PATH, log_dir: Path = DEFAULT_LOG_DIR) -> None:
171
+ """Initialize the session store.
172
+
173
+ :param db_path: SQLite database path.
174
+ :param log_dir: Directory for per-session log files.
175
+ """
176
+ self.db_path = db_path
177
+ self.log_dir = log_dir
178
+
179
+ def log_file_path(self, session_id: str) -> Path:
180
+ """Return the on-disk log file path for a session."""
181
+ return self.log_dir / f"{session_id}.log"
182
+
183
+ async def initialize(self) -> None:
184
+ """Create database tables if they do not exist."""
185
+ if self.db_path.parent != Path("."):
186
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
187
+ self.log_dir.mkdir(parents=True, exist_ok=True)
188
+ async with aiosqlite.connect(self.db_path) as database:
189
+ await database.execute(
190
+ """
191
+ CREATE TABLE IF NOT EXISTS sessions (
192
+ session_id TEXT PRIMARY KEY,
193
+ payload TEXT NOT NULL,
194
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
195
+ )
196
+ """
197
+ )
198
+ await database.execute(
199
+ """
200
+ CREATE TABLE IF NOT EXISTS logs (
201
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
202
+ session_id TEXT NOT NULL,
203
+ entry_type TEXT NOT NULL,
204
+ message TEXT NOT NULL,
205
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
206
+ )
207
+ """
208
+ )
209
+ await self._ensure_sessions_timestamp_column(database)
210
+ await database.commit()
211
+
212
+ async def _ensure_sessions_timestamp_column(self, database: aiosqlite.Connection) -> None:
213
+ """Add ``updated_at`` to legacy session tables when missing."""
214
+ cursor = await database.execute("PRAGMA table_info(sessions)")
215
+ rows = await cursor.fetchall()
216
+ column_names = {str(row[1]) for row in rows}
217
+ if "updated_at" not in column_names:
218
+ # SQLite only allows constant defaults in ALTER TABLE ADD COLUMN.
219
+ await database.execute("ALTER TABLE sessions ADD COLUMN updated_at TEXT NOT NULL DEFAULT ''")
220
+ await database.execute("UPDATE sessions SET updated_at = CURRENT_TIMESTAMP WHERE updated_at = ''")
221
+
222
+ async def save_session(self, session: GameSession) -> None:
223
+ """Insert or update a session."""
224
+ await self.initialize()
225
+ payload = json.dumps(session.to_dict(), sort_keys=True)
226
+ async with aiosqlite.connect(self.db_path) as database:
227
+ await database.execute(
228
+ """
229
+ INSERT INTO sessions (session_id, payload, updated_at)
230
+ VALUES (?, ?, CURRENT_TIMESTAMP)
231
+ ON CONFLICT(session_id) DO UPDATE SET
232
+ payload = excluded.payload,
233
+ updated_at = CURRENT_TIMESTAMP
234
+ """,
235
+ (session.session_id, payload),
236
+ )
237
+ await self._enforce_max_sessions(database)
238
+ await database.commit()
239
+
240
+ async def _enforce_max_sessions(self, database: aiosqlite.Connection) -> None:
241
+ """Delete oldest sessions when the store exceeds ``MAX_SESSIONS``."""
242
+ cursor = await database.execute("SELECT COUNT(*) FROM sessions")
243
+ row = await cursor.fetchone()
244
+ session_count = int(row[0]) if row is not None else 0
245
+ overflow = session_count - MAX_SESSIONS
246
+ if overflow <= 0:
247
+ return
248
+
249
+ cursor = await database.execute(
250
+ """
251
+ SELECT session_id FROM sessions
252
+ ORDER BY updated_at ASC, session_id ASC
253
+ LIMIT ?
254
+ """,
255
+ (overflow,),
256
+ )
257
+ rows = await cursor.fetchall()
258
+ for row in rows:
259
+ session_id = str(row[0])
260
+ await database.execute("DELETE FROM sessions WHERE session_id = ?", (session_id,))
261
+ await database.execute("DELETE FROM logs WHERE session_id = ?", (session_id,))
262
+ self._remove_session_log_file(session_id)
263
+
264
+ async def delete_session(self, session_id: str) -> None:
265
+ """Delete persisted session state and log entries for one session."""
266
+ await self.initialize()
267
+ async with aiosqlite.connect(self.db_path) as database:
268
+ await database.execute("DELETE FROM sessions WHERE session_id = ?", (session_id,))
269
+ await database.execute("DELETE FROM logs WHERE session_id = ?", (session_id,))
270
+ await database.commit()
271
+ self._remove_session_log_file(session_id)
272
+
273
+ def delete_session_callback(self) -> Callable[[str, object], None]:
274
+ """Return a callback that deletes persisted data for one session id.
275
+
276
+ The callback matches :class:`~career_emulator.lru_dict.LRUDict` eviction
277
+ signatures and schedules :meth:`delete_session` on the running loop.
278
+ """
279
+ store = self
280
+
281
+ def callback(session_id: str, _value: object) -> None:
282
+ try:
283
+ loop = asyncio.get_running_loop()
284
+ except RuntimeError:
285
+ store._remove_session_log_file(session_id)
286
+ return
287
+ loop.create_task(store.delete_session(session_id))
288
+
289
+ return callback
290
+
291
+ def _remove_session_log_file(self, session_id: str) -> None:
292
+ """Remove a session log file and close its logger."""
293
+ _close_session_logger(session_id)
294
+ log_path = self.log_file_path(session_id)
295
+ if log_path.exists():
296
+ log_path.unlink()
297
+
298
+ async def load_session(self, session_id: str) -> GameSession | None:
299
+ """Load a session by id."""
300
+ await self.initialize()
301
+ async with aiosqlite.connect(self.db_path) as database:
302
+ cursor = await database.execute("SELECT payload FROM sessions WHERE session_id = ?", (session_id,))
303
+ row = await cursor.fetchone()
304
+ if row is None:
305
+ return None
306
+ payload = json.loads(str(row[0]))
307
+ return GameSession.from_dict(payload)
308
+
309
+ async def append_log(self, entry: LogEntry) -> None:
310
+ """Append one log entry to SQLite and the session log file."""
311
+ await self.initialize()
312
+ log_path = self.log_file_path(entry.session_id)
313
+ logger = _session_logger(entry.session_id, log_path)
314
+ if not log_path.exists() or log_path.stat().st_size == 0:
315
+ logger.info(f"Log file: {log_path.resolve()}")
316
+
317
+ formatted = f"[{entry.entry_type}] {entry.message}"
318
+ logger.info(formatted)
319
+
320
+ async with aiosqlite.connect(self.db_path) as database:
321
+ await database.execute(
322
+ "INSERT INTO logs (session_id, entry_type, message) VALUES (?, ?, ?)",
323
+ (entry.session_id, entry.entry_type, entry.message),
324
+ )
325
+ await database.commit()
326
+
327
+ async def load_logs(self, session_id: str) -> list[LogEntry]:
328
+ """Load logs for a session in insertion order."""
329
+ await self.initialize()
330
+ async with aiosqlite.connect(self.db_path) as database:
331
+ cursor = await database.execute(
332
+ "SELECT session_id, entry_type, message FROM logs WHERE session_id = ? ORDER BY id",
333
+ (session_id,),
334
+ )
335
+ rows = await cursor.fetchall()
336
+ return [LogEntry(session_id=str(row[0]), entry_type=str(row[1]), message=str(row[2])) for row in rows]
337
+
338
+ async def load_latest_logs(self, session_id: str, count: int = 10) -> list[LogEntry]:
339
+ """Load the most recent log entries for a session in insertion order."""
340
+ await self.initialize()
341
+ limit = max(1, count)
342
+ async with aiosqlite.connect(self.db_path) as database:
343
+ cursor = await database.execute(
344
+ """
345
+ SELECT session_id, entry_type, message FROM logs
346
+ WHERE session_id = ?
347
+ ORDER BY id DESC
348
+ LIMIT ?
349
+ """,
350
+ (session_id, limit),
351
+ )
352
+ rows = await cursor.fetchall()
353
+ entries = [LogEntry(session_id=str(row[0]), entry_type=str(row[1]), message=str(row[2])) for row in rows]
354
+ entries.reverse()
355
+ return entries
356
+
357
+ def log_file_header(self, session_id: str) -> str:
358
+ """Return the log file path header for a session."""
359
+ return f"Log file: {self.log_file_path(session_id).resolve()}"
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: career_emulator_engine
3
+ Version: 1.0.1
4
+ Summary: TechArena English track: career simulation engine (CLI + MCP). PyPI: career-emulator-engine.
5
+ Author-email: Yuwei <yuwei.willow.z@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/RAI-Internal/techarena-career-sim
8
+ Project-URL: Documentation, https://career-emulator.readthedocs.io
9
+ Project-URL: Source, https://github.com/RAI-Internal/career-emulator-engine
10
+ Project-URL: Issues, https://github.com/RAI-Internal/techarena-career-sim/issues
11
+ Keywords: python,career-sim,simulation,agent,llm,techarena,competition,mcp client,client,mcp,fastmcp,mcp server,model context protocol
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: Implementation :: CPython
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Intended Audience :: Developers
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: <4,>=3.11
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: msgpack>=1.0.0
27
+ Requires-Dist: aiosqlite>=0.22.1
28
+ Requires-Dist: aiofiles>=25.1.0
29
+ Requires-Dist: career-emulator-techarena==1.0.*
30
+ Provides-Extra: mcp
31
+ Requires-Dist: fastmcp>=2; extra == "mcp"
32
+
33
+ # career-emulator-engine
34
+
35
+ English **TechArena** simulation engine (CLI + MCP).
36
+ Separate from [`career-emulator`](https://pypi.org/project/career-emulator/) (BDCI / Chinese) and from the dataset [`career-emulator-techarena`](https://pypi.org/project/career-emulator-techarena/).
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install "career-emulator-engine[mcp]"
42
+ ```
43
+
44
+ ## Quick use
45
+
46
+ ```bash
47
+ career-emulator-engine update --source distribution --split dev
48
+ career-emulator-engine play
49
+ career-emulator-engine-mcp --help
50
+ ```
51
+
52
+ Participant starter: [techarena-career-sim](https://github.com/RAI-Internal/techarena-career-sim)
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ career_emulator/storage.py
4
+ career_emulator_engine.egg-info/PKG-INFO
5
+ career_emulator_engine.egg-info/SOURCES.txt
6
+ career_emulator_engine.egg-info/dependency_links.txt
7
+ career_emulator_engine.egg-info/entry_points.txt
8
+ career_emulator_engine.egg-info/requires.txt
9
+ career_emulator_engine.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ career-emulator-engine = career_emulator.cli:main
3
+ career-emulator-engine-mcp = career_emulator.run_mcp:main
@@ -0,0 +1,7 @@
1
+ msgpack>=1.0.0
2
+ aiosqlite>=0.22.1
3
+ aiofiles>=25.1.0
4
+ career-emulator-techarena==1.0.*
5
+
6
+ [mcp]
7
+ fastmcp>=2
@@ -0,0 +1,107 @@
1
+ [project]
2
+ name = "career_emulator_engine"
3
+ version = "1.0.1"
4
+ description = "TechArena English track: career simulation engine (CLI + MCP). PyPI: career-emulator-engine."
5
+ keywords = [
6
+ "python", "career-sim", "simulation", "agent", "llm", "techarena", "competition",
7
+ "mcp client", "client", "mcp", "fastmcp", "mcp server", "model context protocol",
8
+ ]
9
+ readme = "README.md"
10
+ requires-python = ">=3.11,<4"
11
+ license = "MIT"
12
+ authors = [
13
+ {name = "Yuwei", email = "yuwei.willow.z@gmail.com"}]
14
+ classifiers = [
15
+ 'Development Status :: 4 - Beta',
16
+ 'Programming Language :: Python',
17
+ 'Programming Language :: Python :: Implementation :: CPython',
18
+ 'Programming Language :: Python :: 3',
19
+ 'Programming Language :: Python :: 3 :: Only',
20
+ 'Programming Language :: Python :: 3.11',
21
+ 'Programming Language :: Python :: 3.12',
22
+ 'Programming Language :: Python :: 3.13',
23
+ 'Programming Language :: Python :: 3.14',
24
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
25
+ "Intended Audience :: Developers",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = [
29
+ "msgpack>=1.0.0",
30
+ "aiosqlite>=0.22.1",
31
+ "aiofiles>=25.1.0",
32
+ "career-emulator-techarena==1.0.*",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ mcp = ["fastmcp>=2"]
37
+
38
+ [dependency-groups]
39
+ test = [
40
+ "pytest", "pytest-asyncio", "pytest-mock", "numpy==2.*", "scipy", "scikit-learn",
41
+ ]
42
+
43
+ docs = [
44
+ "sphinx",
45
+ "sphinx-rtd-theme",
46
+ "beautifulsoup4",
47
+ "myst-parser>=3.0.1,<4",
48
+ "career-emulator-engine[mcp]",
49
+ ]
50
+
51
+ format = [
52
+ "ruff",
53
+ "mypy",
54
+ "types-aiofiles",
55
+ "msgpack-types",
56
+ ]
57
+
58
+ binary = [
59
+ "career-emulator-engine[mcp]",
60
+ "pyinstaller>=6.0,<7",
61
+ "python-minifier>=3.2,<4",
62
+ ]
63
+
64
+ dev = [
65
+ { include-group = "test" },
66
+ { include-group = "format" },
67
+ { include-group = "docs" },
68
+ "setuptools",
69
+ ]
70
+
71
+ [project.urls]
72
+ Homepage = "https://github.com/RAI-Internal/techarena-career-sim"
73
+ Documentation = "https://career-emulator.readthedocs.io"
74
+ Source = "https://github.com/RAI-Internal/career-emulator-engine"
75
+ Issues = "https://github.com/RAI-Internal/techarena-career-sim/issues"
76
+
77
+ [project.scripts]
78
+ career-emulator-engine = "career_emulator.cli:main"
79
+ career-emulator-engine-mcp = "career_emulator.run_mcp:main"
80
+
81
+ [tool.uv]
82
+ default-groups = ['dev']
83
+
84
+ [tool.setuptools.package-data]
85
+ career_emulator = ["**/py.typed", "**/.gitkeep"]
86
+
87
+ [tool.coverage.run]
88
+ omit = ["tests/*"]
89
+
90
+ [tool.pytest.ini_options]
91
+ asyncio_default_fixture_loop_scope = "function"
92
+ testpaths = ["tests"]
93
+ addopts = ["-v"]
94
+
95
+ [tool.ruff]
96
+ line-length = 120
97
+ target-version = "py311"
98
+
99
+ [tool.ruff.lint.pydocstyle]
100
+ convention = "pep257"
101
+
102
+ [tool.pylint.MAIN]
103
+ ignore-paths = ["^(examples|tests|docs|scripts)/.*"]
104
+ py-version = "3.11"
105
+
106
+ [tool.pylint.FORMAT]
107
+ max-line-length = 120
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+