faker-orgchart 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,21 @@
1
+ # Tests on every push and pull request. GitHub-hosted runners only.
2
+ name: ci
3
+ on:
4
+ push:
5
+ pull_request:
6
+ permissions:
7
+ contents: read
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python: ["3.10", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python }}
19
+ - run: pip install '.[test]' ruff
20
+ - run: ruff check src tests
21
+ - run: pytest -q
@@ -0,0 +1,38 @@
1
+ # Publish to PyPI when a version tag (v*) is pushed, through PyPI Trusted Publishing:
2
+ # no token is stored anywhere. PyPI must list this repo, this workflow and the `pypi`
3
+ # environment as the project's trusted publisher.
4
+ name: publish
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+ permissions:
9
+ contents: read
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.13"
18
+ - run: pip install '.[test]' build
19
+ - run: pytest -q
20
+ - name: The tag matches the version
21
+ run: test "v$(python -c 'import tomllib;print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')" = "$GITHUB_REF_NAME"
22
+ - run: python -m build
23
+ - uses: actions/upload-artifact@v4
24
+ with:
25
+ name: dist
26
+ path: dist/
27
+ publish:
28
+ needs: build
29
+ runs-on: ubuntu-latest
30
+ environment: pypi
31
+ permissions:
32
+ id-token: write
33
+ steps:
34
+ - uses: actions/download-artifact@v4
35
+ with:
36
+ name: dist
37
+ path: dist/
38
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,4 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ dist/
4
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steve Walker
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,85 @@
1
+ Metadata-Version: 2.5
2
+ Name: faker-orgchart
3
+ Version: 0.1.0
4
+ Summary: Faker provider for org charts: job levels, leadership titles, and who reports to whom.
5
+ Project-URL: Homepage, https://github.com/middleforkgis/faker-orgchart
6
+ Project-URL: Issues, https://github.com/middleforkgis/faker-orgchart/issues
7
+ Author: Steve Walker
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: fake data,faker,org chart,provider,test data
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Testing
13
+ Classifier: Topic :: Utilities
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: faker>=20
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest; extra == 'test'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # faker-orgchart
21
+
22
+ A [Faker](https://github.com/joke2k/faker) provider for fake org charts: job levels,
23
+ leadership titles, and who reports to whom.
24
+
25
+ Faker's `job` provider returns flat titles ("Bonds trader", "Chief Financial Officer").
26
+ It has no seniority and no reporting line, so it can't give you "a senior manager, their
27
+ VP, and the C-suite above them". This provider does, for demos and tests that need people
28
+ at different levels of a hierarchy, such as approval chains, sign-offs and escalation paths.
29
+ Everything is fictional, and seeding Faker gives you the same org every time.
30
+
31
+ ```python
32
+ from faker import Faker
33
+ from faker_orgchart import OrgChartProvider, reporting_line
34
+
35
+ fake = Faker()
36
+ fake.add_provider(OrgChartProvider)
37
+ fake.seed_instance(7)
38
+
39
+ fake.job_level() # 'senior_manager'
40
+ fake.job_level_label("vp") # 'Vice President'
41
+ fake.leadership_title("c_suite", "Finance") # 'Chief Financial Officer'
42
+ fake.leadership_title("director", "Sales") # 'Director of Sales'
43
+
44
+ # One person per level, from a senior manager up to the CEO:
45
+ for p in fake.reporting_chain("Finance", from_level="senior_manager"):
46
+ print(p["level"], p["title"], p["name"], "reports to", p["manager_id"])
47
+
48
+ # A small company as a tree (CEO first; every manager_id points to someone earlier):
49
+ chart = fake.org_chart(functions=["Finance", "Engineering"], span=(1, 3), down_to="manager")
50
+ reporting_line(chart, chart[-1]["id"]) # that person and everyone above them
51
+ ```
52
+
53
+ ## The ladder
54
+
55
+ | Level key | Label | Example title (Finance) |
56
+ |---|---|---|
57
+ | `c_suite` | C-suite | Chief Financial Officer (the CEO is `c_suite` in function "Executive") |
58
+ | `vp` | Vice President | Vice President, Finance |
59
+ | `director` | Director | Director of Finance |
60
+ | `senior_manager` | Senior Manager | Senior Manager, Finance |
61
+ | `manager` | Manager | Finance Manager |
62
+ | `individual_contributor` | Individual Contributor | Financial Analyst |
63
+
64
+ Functions: Finance, Engineering, Operations, Sales, Marketing, People, Legal and Product,
65
+ each with its chief and a few individual-contributor titles. The titles are English
66
+ (en_US style) for now. Other locales are welcome.
67
+
68
+ Each person is a plain dict: `id`, `name`, `level`, `title`, `function` and `manager_id`.
69
+
70
+ ## Develop
71
+
72
+ ```sh
73
+ pip install -e '.[test]'
74
+ pytest
75
+ ```
76
+
77
+ ## License
78
+
79
+ MIT, like Faker.
80
+
81
+ ## Releasing
82
+
83
+ Bump `version` in `pyproject.toml`, then tag and push the tag (`git tag v0.1.1 && git push
84
+ origin v0.1.1`). The `publish` workflow tests, builds and publishes to PyPI through
85
+ Trusted Publishing, so no token is stored.
@@ -0,0 +1,66 @@
1
+ # faker-orgchart
2
+
3
+ A [Faker](https://github.com/joke2k/faker) provider for fake org charts: job levels,
4
+ leadership titles, and who reports to whom.
5
+
6
+ Faker's `job` provider returns flat titles ("Bonds trader", "Chief Financial Officer").
7
+ It has no seniority and no reporting line, so it can't give you "a senior manager, their
8
+ VP, and the C-suite above them". This provider does, for demos and tests that need people
9
+ at different levels of a hierarchy, such as approval chains, sign-offs and escalation paths.
10
+ Everything is fictional, and seeding Faker gives you the same org every time.
11
+
12
+ ```python
13
+ from faker import Faker
14
+ from faker_orgchart import OrgChartProvider, reporting_line
15
+
16
+ fake = Faker()
17
+ fake.add_provider(OrgChartProvider)
18
+ fake.seed_instance(7)
19
+
20
+ fake.job_level() # 'senior_manager'
21
+ fake.job_level_label("vp") # 'Vice President'
22
+ fake.leadership_title("c_suite", "Finance") # 'Chief Financial Officer'
23
+ fake.leadership_title("director", "Sales") # 'Director of Sales'
24
+
25
+ # One person per level, from a senior manager up to the CEO:
26
+ for p in fake.reporting_chain("Finance", from_level="senior_manager"):
27
+ print(p["level"], p["title"], p["name"], "reports to", p["manager_id"])
28
+
29
+ # A small company as a tree (CEO first; every manager_id points to someone earlier):
30
+ chart = fake.org_chart(functions=["Finance", "Engineering"], span=(1, 3), down_to="manager")
31
+ reporting_line(chart, chart[-1]["id"]) # that person and everyone above them
32
+ ```
33
+
34
+ ## The ladder
35
+
36
+ | Level key | Label | Example title (Finance) |
37
+ |---|---|---|
38
+ | `c_suite` | C-suite | Chief Financial Officer (the CEO is `c_suite` in function "Executive") |
39
+ | `vp` | Vice President | Vice President, Finance |
40
+ | `director` | Director | Director of Finance |
41
+ | `senior_manager` | Senior Manager | Senior Manager, Finance |
42
+ | `manager` | Manager | Finance Manager |
43
+ | `individual_contributor` | Individual Contributor | Financial Analyst |
44
+
45
+ Functions: Finance, Engineering, Operations, Sales, Marketing, People, Legal and Product,
46
+ each with its chief and a few individual-contributor titles. The titles are English
47
+ (en_US style) for now. Other locales are welcome.
48
+
49
+ Each person is a plain dict: `id`, `name`, `level`, `title`, `function` and `manager_id`.
50
+
51
+ ## Develop
52
+
53
+ ```sh
54
+ pip install -e '.[test]'
55
+ pytest
56
+ ```
57
+
58
+ ## License
59
+
60
+ MIT, like Faker.
61
+
62
+ ## Releasing
63
+
64
+ Bump `version` in `pyproject.toml`, then tag and push the tag (`git tag v0.1.1 && git push
65
+ origin v0.1.1`). The `publish` workflow tests, builds and publishes to PyPI through
66
+ Trusted Publishing, so no token is stored.
@@ -0,0 +1,39 @@
1
+ # Draft: issue for joke2k/faker (not posted)
2
+
3
+ Status: draft, 2026-11-04. Post only with the author's go-ahead, from their own GitHub
4
+ account. Faker's CONTRIBUTING asks AI-assisted reports to say so, which the last section does.
5
+
6
+ Before posting, check again that nothing like this has been proposed: search the issues
7
+ for "seniority", "job level" and "org chart" (none found on 2026-11-04).
8
+
9
+ ---
10
+
11
+ **Title:** Job provider: add `job_seniority()` (seniority levels), as the Ruby and Rust ports have
12
+
13
+ **Body:**
14
+
15
+ The `job` provider returns flat titles such as "Bonds trader" or "Chief Financial
16
+ Officer", with nothing about seniority. Two other Faker ports already offer it:
17
+ - Ruby Faker: `Faker::Job.seniority`, which returns values such as "Lead";
18
+ - Rust `fake`: `faker::job::Seniority`.
19
+
20
+ A small, general-purpose addition would fill the gap:
21
+
22
+ ```python
23
+ fake.job_seniority() # e.g. "Senior", "Lead", "Principal", "Junior"
24
+ ```
25
+
26
+ This is a locale-aware word list, following the existing `job` provider's layout, starting
27
+ with en_US and open to other locales. It's useful for test data in HR, recruiting and
28
+ access-control systems, where a level matters as much as a title.
29
+
30
+ Anything more domain-specific, such as reporting lines and whole org charts, I've packaged
31
+ separately as a community provider, following CONTRIBUTING:
32
+ [faker-orgchart](<link once published>). I'm happy to open a PR for it to be listed on
33
+ the community providers page.
34
+
35
+ If this sounds useful I'll open a PR with the en_US list and tests.
36
+
37
+ *AI disclosure:* Claude (Anthropic) was used to survey existing Faker issues and the other
38
+ ports, and to draft this text. The author reviewed it and checked the facts above against
39
+ Faker 40.44.0 and the Ruby and Rust projects' documentation.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "faker-orgchart"
7
+ version = "0.1.0"
8
+ description = "Faker provider for org charts: job levels, leadership titles, and who reports to whom."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Steve Walker" }]
13
+ requires-python = ">=3.10"
14
+ dependencies = ["faker>=20"]
15
+ keywords = ["faker", "fake data", "org chart", "test data", "provider"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Software Development :: Testing",
19
+ "Topic :: Utilities",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/middleforkgis/faker-orgchart"
24
+ Issues = "https://github.com/middleforkgis/faker-orgchart/issues"
25
+
26
+ [project.optional-dependencies]
27
+ test = ["pytest"]
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/faker_orgchart"]
31
+
32
+ [tool.ruff]
33
+ line-length = 120
@@ -0,0 +1,166 @@
1
+ """Fake org charts for Faker: job levels, leadership titles, and who reports to whom.
2
+
3
+ Faker's `job` provider returns flat titles ("Bonds trader", "Chief Financial Officer"),
4
+ with no seniority and no reporting line. This provider adds the ladder:
5
+
6
+ >>> from faker import Faker
7
+ >>> from faker_orgchart import OrgChartProvider
8
+ >>> fake = Faker()
9
+ >>> fake.add_provider(OrgChartProvider)
10
+ >>> fake.job_level() # e.g. 'senior_manager'
11
+ >>> fake.leadership_title("vp", "Finance") # 'Vice President, Finance'
12
+ >>> chain = fake.reporting_chain("Finance") # a manager up to the CEO, one person per level
13
+ >>> chart = fake.org_chart() # a whole (small) company, as a tree
14
+
15
+ Every person is a plain dict: `id`, `name`, `level`, `title`, `function`, `manager_id`
16
+ (None for the CEO). Seed Faker and the same org comes back every time.
17
+ """
18
+
19
+ from collections.abc import Sequence
20
+
21
+ from faker.providers import BaseProvider
22
+
23
+ # The ladder, top down. Keys are stable identifiers; labels are for people.
24
+ LEVELS: tuple[str, ...] = ("c_suite", "vp", "director", "senior_manager", "manager", "individual_contributor")
25
+ LEVEL_LABELS: dict[str, str] = {
26
+ "c_suite": "C-suite",
27
+ "vp": "Vice President",
28
+ "director": "Director",
29
+ "senior_manager": "Senior Manager",
30
+ "manager": "Manager",
31
+ "individual_contributor": "Individual Contributor",
32
+ }
33
+
34
+ # Each function's chief, and a few individual-contributor titles in it.
35
+ FUNCTIONS: dict[str, dict] = {
36
+ "Finance": {"chief": "Chief Financial Officer",
37
+ "ic": ("Financial Analyst", "Accountant", "Senior Accountant", "FP&A Analyst", "Controller")},
38
+ "Engineering": {"chief": "Chief Technology Officer",
39
+ "ic": ("Software Engineer", "Senior Software Engineer", "Site Reliability Engineer",
40
+ "Data Engineer", "QA Engineer")},
41
+ "Operations": {"chief": "Chief Operating Officer",
42
+ "ic": ("Operations Analyst", "Logistics Coordinator", "Process Engineer", "Buyer")},
43
+ "Sales": {"chief": "Chief Revenue Officer",
44
+ "ic": ("Account Executive", "Sales Development Representative", "Solutions Engineer")},
45
+ "Marketing": {"chief": "Chief Marketing Officer",
46
+ "ic": ("Marketing Specialist", "Content Strategist", "Marketing Analyst")},
47
+ "People": {"chief": "Chief People Officer",
48
+ "ic": ("HR Generalist", "Recruiter", "People Operations Specialist")},
49
+ "Legal": {"chief": "Chief Legal Officer",
50
+ "ic": ("Corporate Counsel", "Paralegal", "Compliance Analyst")},
51
+ "Product": {"chief": "Chief Product Officer",
52
+ "ic": ("Product Manager", "Product Designer", "UX Researcher")},
53
+ }
54
+ CEO_TITLE = "Chief Executive Officer"
55
+ EXECUTIVE = "Executive" # the CEO's function
56
+
57
+
58
+ def _check_level(level: str) -> None:
59
+ if level not in LEVELS:
60
+ raise ValueError(f"unknown level {level!r}; choose from {', '.join(LEVELS)}")
61
+
62
+
63
+ def _check_function(function: str) -> None:
64
+ if function not in FUNCTIONS:
65
+ raise ValueError(f"unknown function {function!r}; choose from {', '.join(FUNCTIONS)}")
66
+
67
+
68
+ class OrgChartProvider(BaseProvider):
69
+ """Job levels, leadership titles, reporting chains and org charts."""
70
+
71
+ def job_level(self) -> str:
72
+ """A level on the ladder, e.g. 'senior_manager' (see LEVELS)."""
73
+ return self.random_element(LEVELS)
74
+
75
+ def job_level_label(self, level: str | None = None) -> str:
76
+ """A level as people say it, e.g. 'Senior Manager'."""
77
+ level = level or self.job_level()
78
+ _check_level(level)
79
+ return LEVEL_LABELS[level]
80
+
81
+ def business_function(self) -> str:
82
+ """A business function, e.g. 'Finance' (see FUNCTIONS)."""
83
+ return self.random_element(tuple(FUNCTIONS))
84
+
85
+ def leadership_title(self, level: str | None = None, function: str | None = None) -> str:
86
+ """A title for `level` in `function`: 'Chief Financial Officer', 'Director of Finance', ..."""
87
+ level = level or self.job_level()
88
+ function = function or self.business_function()
89
+ _check_level(level)
90
+ _check_function(function)
91
+ if level == "c_suite":
92
+ return FUNCTIONS[function]["chief"]
93
+ if level == "vp":
94
+ return self.random_element((f"Vice President, {function}", f"VP of {function}"))
95
+ if level == "director":
96
+ return self.random_element((f"Director of {function}", f"{function} Director"))
97
+ if level == "senior_manager":
98
+ return f"Senior Manager, {function}"
99
+ if level == "manager":
100
+ return f"{function} Manager"
101
+ return self.random_element(FUNCTIONS[function]["ic"])
102
+
103
+ def _person(self, pid: int, level: str, function: str, manager_id: int | None, title: str | None = None) -> dict:
104
+ return {"id": pid, "name": self.generator.name(), "level": level,
105
+ "title": title or self.leadership_title(level, function), "function": function,
106
+ "manager_id": manager_id}
107
+
108
+ def reporting_chain(self, function: str | None = None, from_level: str = "manager") -> list[dict]:
109
+ """One person per level, from `from_level` up to the CEO, each reporting to the next.
110
+
111
+ For a demo that needs "a senior manager, their VP, and the C-suite above them".
112
+ """
113
+ function = function or self.business_function()
114
+ _check_function(function)
115
+ _check_level(from_level)
116
+ levels = LEVELS[: LEVELS.index(from_level) + 1] # top down
117
+ chain = [self._person(0, "c_suite", EXECUTIVE, None, CEO_TITLE)]
118
+ for i, level in enumerate(levels, start=1):
119
+ chain.append(self._person(i, level, function, chain[-1]["id"]))
120
+ return list(reversed(chain)) # bottom up: the first one reports to the second, ...
121
+
122
+ def org_chart(self, functions: Sequence[str] | None = None, span: tuple[int, int] = (1, 3),
123
+ down_to: str = "manager") -> list[dict]:
124
+ """A company as a tree: the CEO; a chief per function; below each, `span` reports per level.
125
+
126
+ `down_to` is the lowest level included (default 'manager', so the chart stays small;
127
+ 'individual_contributor' adds the teams). The CEO comes first; every other person's
128
+ `manager_id` is the id of someone earlier in the list.
129
+ """
130
+ functions = list(functions) if functions is not None else self.random_elements(
131
+ tuple(FUNCTIONS), length=self.random_int(3, 5), unique=True)
132
+ for f in functions:
133
+ _check_function(f)
134
+ _check_level(down_to)
135
+ low, high = span
136
+ if not 1 <= low <= high:
137
+ raise ValueError(f"span must be (low, high) with 1 <= low <= high, got {span}")
138
+ levels = LEVELS[1: LEVELS.index(down_to) + 1] # below the chiefs
139
+ people = [self._person(0, "c_suite", EXECUTIVE, None, CEO_TITLE)]
140
+
141
+ def add(level: str, function: str, manager_id: int) -> int:
142
+ people.append(self._person(len(people), level, function, manager_id))
143
+ return people[-1]["id"]
144
+
145
+ for function in functions:
146
+ frontier = [add("c_suite", function, 0)]
147
+ for level in levels:
148
+ frontier = [add(level, function, m) for m in frontier for _ in range(self.random_int(low, high))]
149
+ return people
150
+
151
+
152
+ def reporting_line(chart: Sequence[dict], person_id: int) -> list[dict]:
153
+ """`person_id` and everyone above them, up to the top."""
154
+ by_id = {p["id"]: p for p in chart}
155
+ line = [by_id[person_id]]
156
+ while line[-1]["manager_id"] is not None:
157
+ line.append(by_id[line[-1]["manager_id"]])
158
+ return line
159
+
160
+
161
+ def direct_reports(chart: Sequence[dict], person_id: int) -> list[dict]:
162
+ return [p for p in chart if p["manager_id"] == person_id]
163
+
164
+
165
+ __all__ = ["CEO_TITLE", "FUNCTIONS", "LEVELS", "LEVEL_LABELS", "OrgChartProvider", "direct_reports",
166
+ "reporting_line"]
@@ -0,0 +1,74 @@
1
+ import itertools
2
+
3
+ import pytest
4
+ from faker import Faker
5
+
6
+ from faker_orgchart import (
7
+ CEO_TITLE,
8
+ FUNCTIONS,
9
+ LEVELS,
10
+ OrgChartProvider,
11
+ direct_reports,
12
+ reporting_line,
13
+ )
14
+
15
+
16
+ @pytest.fixture
17
+ def fake() -> Faker:
18
+ f = Faker()
19
+ f.add_provider(OrgChartProvider)
20
+ f.seed_instance(2026)
21
+ return f
22
+
23
+
24
+ def test_levels_and_titles(fake):
25
+ assert fake.job_level() in LEVELS
26
+ assert fake.job_level_label("senior_manager") == "Senior Manager"
27
+ assert fake.leadership_title("c_suite", "Finance") == "Chief Financial Officer"
28
+ assert fake.leadership_title("vp", "Finance") in ("Vice President, Finance", "VP of Finance")
29
+ assert fake.leadership_title("senior_manager", "Sales") == "Senior Manager, Sales"
30
+ assert fake.leadership_title("individual_contributor", "Legal") in FUNCTIONS["Legal"]["ic"]
31
+ with pytest.raises(ValueError, match="unknown level"):
32
+ fake.leadership_title("intern", "Finance")
33
+ with pytest.raises(ValueError, match="unknown function"):
34
+ fake.leadership_title("vp", "Astrology")
35
+
36
+
37
+ def test_reporting_chain_climbs_one_level_at_a_time(fake):
38
+ chain = fake.reporting_chain("Finance", from_level="senior_manager")
39
+ assert [p["level"] for p in chain] == ["senior_manager", "director", "vp", "c_suite", "c_suite"]
40
+ assert [p["title"] for p in chain][-2:] == ["Chief Financial Officer", CEO_TITLE]
41
+ for person, boss in itertools.pairwise(chain):
42
+ assert person["manager_id"] == boss["id"]
43
+ assert chain[-1]["manager_id"] is None
44
+
45
+
46
+ def test_org_chart_is_a_tree_under_the_ceo(fake):
47
+ chart = fake.org_chart(functions=["Finance", "Engineering"], span=(2, 2), down_to="director")
48
+ ceo = chart[0]
49
+ assert ceo["title"] == CEO_TITLE and ceo["manager_id"] is None
50
+ # CEO + per function: 1 chief, 2 VPs, 4 directors.
51
+ assert len(chart) == 1 + 2 * (1 + 2 + 4)
52
+ assert {p["title"] for p in direct_reports(chart, ceo["id"])} == {"Chief Financial Officer",
53
+ "Chief Technology Officer"}
54
+ seen = set()
55
+ for p in chart:
56
+ assert p["manager_id"] is None or p["manager_id"] in seen # bosses come first
57
+ seen.add(p["id"])
58
+ if p is not ceo:
59
+ boss = chart[p["manager_id"]]
60
+ assert LEVELS.index(p["level"]) == LEVELS.index(boss["level"]) + (0 if boss is ceo else 1)
61
+ director = next(p for p in chart if p["level"] == "director")
62
+ assert [p["level"] for p in reporting_line(chart, director["id"])] == ["director", "vp", "c_suite", "c_suite"]
63
+
64
+
65
+ def test_seeded_orgs_repeat(fake):
66
+ again = Faker()
67
+ again.add_provider(OrgChartProvider)
68
+ again.seed_instance(2026)
69
+ assert fake.org_chart() == again.org_chart()
70
+
71
+
72
+ def test_bad_span_is_refused(fake):
73
+ with pytest.raises(ValueError, match="span"):
74
+ fake.org_chart(span=(3, 1))