Extensões: como adicionar um novo scanner de ATS¶
Toda integração de ATS embutida (Greenhouse, Lever, Ashby, Recruitee, Workable, SmartRecruiters, InHire) e todo feed de portal (RemoteOK, Remotive, WeWorkRemotely, HN Who's Hiring, Gupy) é parte normal deste repositório — mas o moonlighter também suporta extensões de scanner: pacotes Python separados, instalados de forma independente, que registram uma nova fonte de vagas sem precisar dar fork ou modificar este repositório de jeito nenhum.
É assim que o scan do LinkedIn é distribuído — não porque o mecanismo seja específico do LinkedIn, mas porque os próprios Termos de Uso do LinkedIn proíbem automação de forma explícita e inequívoca (veja o DISCLAIMER.md, em inglês), então essa integração é distribuída como uma extensão opcional em vez de código embutido que qualquer um que clonar este repositório já ganha por padrão.
Preenchimento e envio de formulário via browser não fazem parte deste repositório de jeito nenhum (veja como funciona) e não são um ponto de extensão — o prepare_application compõe as respostas para você colar, para qualquer ATS.
Como funciona uma extensão¶
Uma extensão é um pacote Python normal que:
- Depende de
moonlighter-coreemoonlighter-scan, fixados numa tag publicada deste repositório. - Traz o próprio módulo implementando uma subclasse de
BaseScanner(vejapackages/scan/moonlighter/discovery/sources/base.py). -
Se declara via
entry_pointsno própriopyproject.toml— nenhum código deste repositório importa ou cita a extensão:[project.entry-points."moonlighter.scanners"] my_platform = "my_package.my_module:MyScanner" # Optional: a browser-based staleness check for a source with no listing API [project.entry-points."moonlighter.staleness_checkers"] my_platform = "my_package.my_module:check_staleness"Um scanner baseado em browser (como costumam ser as entradas de
moonlighter.scanners) precisa demoonlighter-core[browser]— veja Requisitos; um scanner puramente HTTP não precisa de nada extra. -
Precisa estar presente no mesmo ambiente Python de onde o moonlighter roda, para que seus entry points sejam descobertos em tempo de execução. Se você instalou o moonlighter via
uvx moonlighter, não existe um ambiente persistente onde adicionar um pacote — use uma das opções:uvx --with my-extension-package moonlighter— efêmero, por invocaçãouv tool install moonlighter --with my-extension-package— instalação persistente da ferramenta
Se você está desenvolvendo direto neste repositório,
uv add --editable/pip installdo pacote da sua extensão no mesmo ambiente continua funcionando como antes. Em tempo de execução,moonlighter.core.plugins.discover_entry_points/discover_entry_points_by_nameenumeram o que estiver registrado em cada grupo — um ambiente sem nenhuma extensão instalada se comporta exatamente como antes (lista/dict vazio, nada quebra).
Como o pacote de nível raiz moonlighter é um namespace package PEP 420 (sem __init__.py nesse nível), uma extensão pode até trazer o próprio subpacote de nível raiz (ex.: moonlighter/my_extension/), que convive com moonlighter.core/moonlighter.discovery/etc. Só não coloque arquivos dentro de um subpacote já existente, como moonlighter/discovery/sources/: esse é um pacote regular (não namespace) que pertence inteiramente às distribuições deste repositório, e uma segunda distribuição escrevendo no mesmo caminho colide silenciosamente na instalação. Dê à sua extensão o próprio diretório de nível raiz.
Exemplo real¶
A extensão privada moonlighter-linkedin (não publicada, pelo motivo acima) segue exatamente esse padrão para o scan — o LinkedInScanner dela vive no próprio pacote moonlighter/linkedin_ext/, registrado pelo grupo de entry points moonlighter.scanners acima. Se você for construir a sua própria extensão de scanner, essa é a forma de referência a copiar.