### Quickstart: open save, get managed club squad, write CSV Source: https://rhiever.github.io/fmsave/index.html Open a save with fmsave.open, get the first managed club, filter players by club_uid, sort by current ability, print top 5, and write to CSV. The comment shows example output. ```python import fmsave with fmsave.open("career.fm") as career_save: my_club = career_save.managed_clubs()[0] # empty when you are between jobs squad = career_save.players().where(club_uid=my_club.club_uid) for player in squad.sorted_by(lambda player: player.ability.current, reverse=True)[:5]: print(player.name, player.age, player.ability.current, player.attributes.finishing) # "Alex Example" 24 148 16 squad.write_csv("squad.csv") ``` -------------------------------- ### Install fmsave with pip and uvx Source: https://rhiever.github.io/fmsave/_sources/index.md.txt Install fmsave using pip or try it without installing via uvx. The pandas extra adds Table.to_pandas(). Requires Python 3.12 or newer. ```console uvx fmsave info path/to/career.fm # try it without installing pip install fmsave pip install "fmsave[pandas]" # adds Table.to_pandas() ``` -------------------------------- ### Export players to CSV with fmsave CLI Source: https://rhiever.github.io/fmsave/_sources/index.md.txt Command-line equivalent of the Python quickstart: exports players from the managed club to a CSV file. ```console fmsave export career.fm players --managed-club -o squad.csv ``` -------------------------------- ### fmsave export command-line examples Source: https://rhiever.github.io/fmsave/_sources/guides/exporting.md.txt Shows four example invocations of the fmsave export command. The scope is required and must be one of --managed-club, --club, --competition, --nation, or --all. The --format option defaults to csv; --columns specifies flat column names; -o writes to a file instead of standard output. ```console fmsave export career.fm players --managed-club -o squad.csv fmsave export career.fm players --managed-club --columns name,age,ability_current,contract_wage fmsave export career.fm players --all --format json -o players.json fmsave export career.fm fixtures --managed-club --format jsonl -o fixtures.jsonl ``` -------------------------------- ### check_managed: start of managed-club reader check Source: https://rhiever.github.io/fmsave/_modules/fmsave/_checks.html Starts the managed-club reader check function, but the source cuts off mid-docstring and does not show the full implementation. ```python def check_managed(stats: ManagedStats, bounds: GateBounds, game_db_bytes: int) -> ReaderCheck: """The managed-club reader's record count and anomaly counts. ``` -------------------------------- ### Open a save and print top squad players Source: https://rhiever.github.io/fmsave/_sources/index.md.txt Open a save file, get the managed club, filter players by club, sort by current ability, and print top five players. Writes squad to CSV. The managed_clubs() list is empty when between jobs. ```python import fmsave with fmsave.open("career.fm") as career_save: my_club = career_save.managed_clubs()[0] # empty when you are between jobs squad = career_save.players().where(club_uid=my_club.club_uid) for player in squad.sorted_by(lambda player: player.ability.current, reverse=True)[:5]: print(player.name, player.age, player.ability.current, player.attributes.finishing) # "Alex Example" 24 148 16 squad.write_csv("squad.csv") ``` -------------------------------- ### Open a save and get your squad Source: https://rhiever.github.io/fmsave/guides/squad.html Opens a save file with fmsave.open as a context manager, gets the first managed club, and filters all players to that club. The comment notes that managed_clubs() is empty when between jobs. Readers must be called inside the with block; asking a closed save raises SaveClosedError. ```python import fmsave with fmsave.open("career.fm") as career_save: my_club = career_save.managed_clubs()[0] # empty when you are between jobs squad = career_save.players().where(club_uid=my_club.club_uid) ``` -------------------------------- ### Determine active week from training calendar Source: https://rhiever.github.io/fmsave/reference/save.html This snippet shows how to find the active week for a team by comparing week start dates to the save's game date. It filters weeks that have started and selects the latest one, handling cases where dates may be None. ```python clock = career_save.info.game_date weeks = career_save.training()[0].weeks started = [ week for week in weeks if week.week_start is not None and clock is not None and week.week_start <= clock ] active = max(started, default=None, key=lambda week: week.week_start) ``` -------------------------------- ### fmsave --help Source: https://rhiever.github.io/fmsave/reference/cli.html Shows the top-level fmsave CLI help, listing the three subcommands (info, export, validate) and the --version flag. The tool reads Football Manager 26 save files and never modifies a save. ```text $ fmsave --help usage: fmsave [-h] [--version] COMMAND ... Read Football Manager 26 save files. fmsave never modifies a save. positional arguments: COMMAND info show the game, build and in-game date of a save export write one table of a save as CSV, JSON or JSON Lines validate report how each reader fares on a save, without any names or uids options: -h, --help show this help message and exit --version show program's version number and exit ``` -------------------------------- ### fmsave.Table.index Source: https://rhiever.github.io/fmsave/reference/table.html Return the position of the first record equal to value between start and stop. ```APIDOC ## fmsave.Table.index ### Description Return the position of the first record equal to value between start and stop. A stop of None means the end of the table. Negative start and stop count from the end. ### Method index ### Parameters - **value** (object) - Required - The value to search for. - **start** (int) - Optional - Starting index. Default is 0. - **stop** (int|None) - Optional - Stopping index. A stop of None means the end of the table. Negative start and stop count from the end. ### Returns - **int** - The position of the first record equal to value. ### Raises - **ValueError** - No such record. ``` -------------------------------- ### fmsave info --help Source: https://rhiever.github.io/fmsave/reference/cli.html Shows the help for the fmsave info subcommand, which displays save metadata. The --show-name flag includes the save's name (and with --json the save summary texts, which hold names hidden by default); --json prints JSON instead of text. ```text $ fmsave info --help usage: fmsave info [-h] [--show-name] [--json] SAVE Show save metadata. positional arguments: SAVE path to a .fm save file options: -h, --help show this help message and exit --show-name include the save's name, and with --json the save summary texts, which hold names (hidden by default) --json print JSON instead of text ``` -------------------------------- ### fmsave.Save.__init__ Source: https://rhiever.github.io/fmsave/reference/save.html Creates a new Save instance for reading a Football Manager 26 save file. The constructor reads only the header, directory, and save metadata; the file is not kept open. ```APIDOC ## fmsave.Save.__init__ ### Description Creates a new Save instance for reading a Football Manager 26 save file. The constructor reads only the header, directory, and save metadata; the file is not kept open. ### Method Constructor ### Signature ```python Save(container_index, info, *, strict=False, competition_names={}) ``` ### Parameters - **container_index** (ContainerIndex) - Required - The container index of the save. - **info** (SaveInfo) - Required - The save information. - **strict** (bool) - Optional - If True, raises ReaderCheckError instead of warning on check failures. Defaults to False. - **competition_names** (FrozenMapping[int, str]) - Optional - A mapping of competition IDs to names. Defaults to {}. ### Notes - The file is not kept open after construction. - Each reader reopens the file, checks that it has not changed since it was opened, and reads only the parts it needs. - A failed check warns with ReaderCheckWarning and the reader hands its table back; a save opened with strict=True raises ReaderCheckError instead. - A structural failure raises either way. - A Save is not thread-safe: use one Save per thread. - Records and tables it returns are immutable, safe to share, and keep working after the Save is closed. ``` -------------------------------- ### finances() Source: https://rhiever.github.io/fmsave/reference/save.html Returns a table of monthly financial records for clubs tracked by the save. Each row represents a club's financial state for a month, including balance, net transfers, and budget information. ```APIDOC ## finances() ### Description Returns a table of monthly financial records for clubs tracked by the save. Each row represents a club's financial state for a month, including balance, net transfers, and budget information. ### Method finances() ### Parameters None ### Return Type _Table_[_FinanceMonth_] ### Raises - **SaveClosedError** – The save is closed. - **SaveChangedError** – The file changed on disk after it was opened. - **CorruptSaveError** – The save is damaged or was being written. - **ReaderCheckError** – No club record is accepted, a club uid or club index appears in two records, a team id is listed twice, or the save’s in-game date is unreadable, so no month can be dated. ### Warns - **ReaderCheckWarning** – On a full-size save, the months and sponsors decoded fall outside the checks’ bounds, or the club table falls outside its own. A save opened with strict=True raises ReaderCheckError instead. ### Notes - Only some clubs have a series at all; the save keeps one for the clubs of the one or two league nations it tracks. - A save whose clubs keep none at all returns an empty table. - balance is the balance at the end of the row’s month, and net_transfers is positive for a net spend. - Money is a whole number in the save’s base currency, which is not the currency the game displays. - month is worked out from the save’s own date rather than stored. - The table is read on the first call to finances() or sponsorships(), from one decode pass; later calls to either return the same tables. ``` -------------------------------- ### Validate a save with fmsave.validate_save Source: https://rhiever.github.io/fmsave/guides/trust.html Run every reader and get a report. It neither raises nor warns. Each reader's status is 'ok', 'failed', or 'error'. ```python with fmsave.open("career.fm") as career_save: report = fmsave.validate_save(career_save) for reader in report.readers: print(reader.reader, reader.status, reader.record_count) # "players" "ok" 124310 ``` -------------------------------- ### fixtures() Source: https://rhiever.github.io/fmsave/reference/save.html Returns a table of all scheduled or played matches, in date and kick-off order. Includes scores for about a quarter of played matches, and stadium information for each match. ```APIDOC ## fixtures() ### Description Returns a table of all scheduled or played matches, in date and kick-off order. Includes scores for about a quarter of played matches, and stadium information for each match. ### Method GET ### Endpoint fixtures() ### Parameters None ### Request Example None ### Response #### Success Response (200) - **date** (date) - The date and kick-off time of the match. - **home_team_id** (int) - The ID of the home team. - **away_team_id** (int) - The ID of the away team. - **home_goals** (int or None) - The number of goals scored by the home team, if the result is stored. - **away_goals** (int or None) - The number of goals scored by the away team, if the result is stored. - **stadium_uid** (int) - The UID of the stadium where the match is played. - **competition_id** (int) - The ID of the competition the match belongs to. - **stage_id** (int) - The ID of the stage within the competition. #### Response Example ```json { "fixtures": [ { "date": "2024-01-15T15:00:00", "home_team_id": 1, "away_team_id": 2, "home_goals": 2, "away_goals": 1, "stadium_uid": 100, "competition_id": 10, "stage_id": 5 } ] } ``` ### Error Handling - **SaveClosedError** – The save is closed. - **SaveChangedError** – The file changed on disk after it was opened. - **CorruptSaveError** – The save is damaged or was being written. - **ReaderCheckError** – No club record is accepted, a club uid or club index appears in two records, a team id is listed twice, no stage table was found in the tail of the game database, a stage id appears in two rows, no stadium table was found, or the save’s in-game date is unreadable. With a competition name map, the competition reader runs first and raises here too, so no row is named from a table that could not be read. ### Warnings - **ReaderCheckWarning** – On a full-size span, the calendar or the scores joined onto it fall outside the checks’ bounds. The stadium table’s checks run here as well, and with a name map the competition table’s, since a row carries a ground and a name from each. A save opened with strict=True raises ReaderCheckError instead. ``` -------------------------------- ### TransferWindow dataclass definition Source: https://rhiever.github.io/fmsave/_modules/fmsave/models/rules.html Defines the frozen dataclass TransferWindow with season-relative date fields and an unknown mapping. The docstring includes a code example for deduplicating repeated windows. ```python @dataclass(frozen=True, slots=True) class TransferWindow: """One transfer window: when it opens and closes, relative to the season start. The window carries no name. The save's rules database does hold description strings, but none of them belongs to a window record: on the saves measured only six windows in fifty-four have a description ending anywhere near the record, and the rest sit hundreds of thousands of bytes away. Attaching the nearest one would put an unrelated label on a window, so no label ships and the dates are the whole of what a window says. **A save holds more rows than it has distinct windows.** On the saves measured the reader returns 54 rows of which 22 are distinct: 17 of those appear twice and 5 appear four times. Rows that repeat are equal in every field this record carries, so a caller who wants one row per distinct window can drop the repeats and lose nothing it is able to see. A record carries a mapping and so is not hashable, which rules out a `set`; equality works as usual, so keep the first of each instead:: distinct_windows: list[TransferWindow] = [] for window in career_save.transfer_windows(): if window not in distinct_windows: distinct_windows.append(window) Why the rules database repeats a window is **not established**. What was measured is that a repeat carries no key of its own that fmsave drops: of the 32 repeated records, 26 repeat the whole underlying record exactly, tag for tag and value for value, including the id each of its two date sub-lists carries, and the other 6 differ only in a description and two unidentified tags. Nothing inside a window record tells one copy from another, so whatever separates them, if anything does, sits outside the record and this reader does not read it. In particular nothing measured here makes them per-competition or per-nation copies; that remains a guess. The count is the anchored walk's own, not a decode that counts a record twice, and every save measured gives the same 54 rows and the same 22 distinct windows. Attributes: start_day: Day of the month the window opens, 1 to 31. start_month: Month the window opens, 1 to 12. start_season_year_offset: Which season year the window opens in: 0 is the season's own start year and 1 the calendar year after it. There is no calendar date without a season, so this is an offset and never a date. end_day: Day of the month the window closes, 1 to 31. end_month: Month the window closes, 1 to 12. end_season_year_offset: Which season year the window closes in, as start_season_year_offset. unknown: Numeric fields with no known meaning. "close_time" is the value stored against the window's closing-time tag, which looks like an hour and minute on a 24-hour clock but is not named, because the game's own Rules screen shows no time of day to pin it to. "window_type" is a small code the record carries on about two windows in five and no displayed label explains (unconfirmed). """ start_day: int start_month: int start_season_year_offset: int end_day: int end_month: int end_season_year_offset: int unknown: Mapping[str, int] UNKNOWN_KEYS: ClassVar[tuple[str, ...]] = ("close_time", "window_type") ``` -------------------------------- ### Get column coverage from a squad Source: https://rhiever.github.io/fmsave/guides/trust.html Open a career save, select a squad, and inspect the coverage of flat columns. A low number is usually the save, not a bug. Values fmsave cannot read are None. ```python with fmsave.open("career.fm") as career_save: squad = career_save.players().where(club_uid=career_save.managed_clubs()[0].club_uid) squad.coverage["contract_wage"] # 1.0 squad.coverage["loan_parent_club_name"] ```