Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 77 additions & 24 deletions staffspy/linkedin/experiences.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,32 +118,85 @@ def parse_experiences(self, elements):

return exps

@staticmethod
def _nested(node, *path):
"""Safely walk a chain of dict keys, returning None if any link is missing."""
cur = node
for key in path:
if not isinstance(cur, dict):
return None
cur = cur.get(key)
return cur

def parse_multi_exp(self, entity):
"""Parse a company entry that holds multiple roles (promotions).

Hardened against LinkedIn's variable payloads: previously this assumed the
``pagedListComponent`` and every per-role field were present, so a missing
node raised ``TypeError``. The caller's ``try/except`` then swallowed it and
dropped the whole experience entry — silently losing tenure for long-tenured
/ promoted members. We now guard every access and fall back to the
company-level caption span when the per-role list is unavailable.
"""
exps = []
company = entity["titleV2"]["text"]["text"]
elements = entity["subComponents"]["components"][0]["components"][
"pagedListComponent"
]["components"]["elements"]
for elem in elements:
entity = elem["components"]["entityComponent"]
duration = entity["caption"]["text"]
title = entity["titleV2"]["text"]["text"]
emp_type = (
entity["subtitle"]["text"].lower() if entity["subtitle"] else None
company = self._nested(entity, "titleV2", "text", "text")

components = self._nested(entity, "subComponents", "components")
role_elements = None
if isinstance(components, list) and components:
role_elements = self._nested(
components[0], "components", "pagedListComponent", "components",
"elements",
)
location = entity["metadata"]["text"] if entity["metadata"] else None
start_date, end_date = utils.parse_dates(duration)
from_date, to_date = utils.parse_duration(duration)
if from_date:
duration = duration.split(" · ")[1]
exp = Experience(
duration=duration,
title=title,
company=company,
emp_type=emp_type,
start_date=start_date,
end_date=end_date,
location=location,

if isinstance(role_elements, list):
for elem in role_elements:
try:
ent = self._nested(elem, "components", "entityComponent")
if not isinstance(ent, dict):
continue
duration = self._nested(ent, "caption", "text")
title = self._nested(ent, "titleV2", "text", "text")
subtitle = self._nested(ent, "subtitle", "text")
emp_type = subtitle.lower() if subtitle else None
location = self._nested(ent, "metadata", "text")
start_date, end_date = (
utils.parse_dates(duration) if duration else (None, None)
)
from_date, _ = (
utils.parse_duration(duration) if duration else (None, None)
)
if from_date and duration and " · " in duration:
duration = duration.split(" · ")[1]
exps.append(
Experience(
duration=duration,
title=title,
company=company,
emp_type=emp_type,
start_date=start_date,
end_date=end_date,
location=location,
)
)
except Exception as e: # one malformed role shouldn't drop the rest
logger.debug(f"skipping malformed role in multi-exp: {e}")

# Fallback: no per-role data parsed — use the company-level caption span,
# which carries the overall tenure across all roles at the company.
if not exps:
duration = self._nested(entity, "caption", "text")
start_date, end_date = (
utils.parse_dates(duration) if duration else (None, None)
)
exps.append(exp)
if company and (start_date or duration):
exps.append(
Experience(
duration=duration,
company=company,
start_date=start_date,
end_date=end_date,
)
)

return exps