src-auth-perms-sync 0.4.0__tar.gz → 0.5.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.
Files changed (186) hide show
  1. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.gitignore +1 -2
  2. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/AGENTS.md +8 -2
  3. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/PKG-INFO +42 -7
  4. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/README.md +40 -5
  5. src_auth_perms_sync-0.5.0/dev/PLAN.md +458 -0
  6. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/TODO.md +9 -21
  7. src_auth_perms_sync-0.5.0/dev/engineering-requests.md +781 -0
  8. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/pyproject.toml +2 -2
  9. src_auth_perms_sync-0.5.0/src/src_auth_perms_sync/__init__.py +36 -0
  10. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/cli.py +244 -92
  11. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/orgs/sync.py +39 -50
  12. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/command.py +189 -170
  13. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/full_set.py +157 -177
  14. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/queries.py +46 -5
  15. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/restore.py +133 -171
  16. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/snapshot.py +133 -16
  17. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/sourcegraph.py +103 -0
  18. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/workflow.py +51 -64
  19. src_auth_perms_sync-0.5.0/src/src_auth_perms_sync/shared/backups.py +159 -0
  20. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/shared/run_context.py +10 -2
  21. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/README.md +15 -0
  22. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/case_runner.py +182 -87
  23. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/add-users-by-email-and-list/after.json +1 -2
  24. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/add-users-by-email-and-list/before.json +1 -2
  25. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/add-users-preserves-existing/after.json +1 -2
  26. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/add-users-preserves-existing/before.json +1 -2
  27. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/and-filters-intersect/after.json +1 -2
  28. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/and-filters-intersect/before.json +1 -2
  29. {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/invalid-bad-regex → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/empty-maps-noop}/before.json +1 -2
  30. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/full-overwrite-dry-run/before.json +1 -2
  31. src_auth_perms_sync-0.5.0/tests/e2e/fixtures/full-overwrite-preserves-pending/after.json +74 -0
  32. src_auth_perms_sync-0.5.0/tests/e2e/fixtures/full-overwrite-preserves-pending/before.json +74 -0
  33. src_auth_perms_sync-0.5.0/tests/e2e/fixtures/full-overwrite-preserves-pending/maps.yaml +8 -0
  34. {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/full-overwrite-with-backup → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/full-overwrite-removes-stale-grant}/after.json +1 -2
  35. {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/full-overwrite-with-backup → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/full-overwrite-removes-stale-grant}/before.json +1 -2
  36. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/full-overwrite-unions/after.json +7 -2
  37. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/full-overwrite-unions/before.json +7 -2
  38. {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/full-overwrite-removes-stale-grant → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/full-overwrite-with-backup}/after.json +4 -2
  39. {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/full-overwrite-removes-stale-grant → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/full-overwrite-with-backup}/before.json +4 -2
  40. {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/get-user-grants → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/get-full-snapshot}/before.json +1 -2
  41. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/get-repos-without-explicit-perms/before.json +1 -2
  42. {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/get-full-snapshot → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/get-user-grants}/before.json +1 -2
  43. {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/empty-maps-noop → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/invalid-bad-regex}/before.json +1 -2
  44. src_auth_perms_sync-0.5.0/tests/e2e/fixtures/invalid-missing-repos-section/before.json +47 -0
  45. src_auth_perms_sync-0.5.0/tests/e2e/fixtures/invalid-restore-wrong-schema-version/before.json +47 -0
  46. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-created-after-date/before.json +1 -2
  47. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-repos-created-after-date/before.json +1 -2
  48. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-unknown-repo/before.json +1 -2
  49. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-unknown-user/before.json +1 -2
  50. src_auth_perms_sync-0.5.0/tests/e2e/fixtures/invalid-unknown-selector-field/before.json +47 -0
  51. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/match-provider-and-host-fields/after.json +1 -2
  52. {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/saml-group-filter → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/match-provider-and-host-fields}/before.json +1 -2
  53. src_auth_perms_sync-0.5.0/tests/e2e/fixtures/no-match-noop/before.json +47 -0
  54. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/regex-filters-scope/after.json +1 -2
  55. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/regex-filters-scope/before.json +1 -2
  56. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/restore-applies-snapshot/after.json +1 -2
  57. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/restore-applies-snapshot/before.json +1 -2
  58. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/restore-applies-snapshot/snapshot.json +2 -2
  59. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/restore-dry-run-noop/before.json +1 -2
  60. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/restore-dry-run-noop/snapshot.json +2 -2
  61. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/restore-missing-file/before.json +1 -2
  62. src_auth_perms_sync-0.5.0/tests/e2e/fixtures/restore-restores-pending/after.json +57 -0
  63. src_auth_perms_sync-0.5.0/tests/e2e/fixtures/restore-restores-pending/before.json +57 -0
  64. src_auth_perms_sync-0.5.0/tests/e2e/fixtures/restore-restores-pending/snapshot.json +30 -0
  65. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/saml-group-filter/after.json +1 -2
  66. {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/match-provider-and-host-fields → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/saml-group-filter}/before.json +1 -2
  67. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/saml-group-live/after.json +1 -2
  68. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/saml-group-live/before.json +1 -2
  69. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-created-after-temp-user/after.json +1 -2
  70. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-created-after-temp-user/before.json +1 -2
  71. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-created-after/after.json +1 -2
  72. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-created-after/before.json +1 -2
  73. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-created-after-noop/before.json +1 -2
  74. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-filter/after.json +1 -2
  75. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-filter/before.json +1 -2
  76. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-without-explicit-perms/after.json +1 -2
  77. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-without-explicit-perms/before.json +1 -2
  78. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-users-created-after/after.json +1 -2
  79. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-users-created-after/before.json +1 -2
  80. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-users-created-after-noop/before.json +1 -2
  81. src_auth_perms_sync-0.5.0/tests/e2e/fixtures/set-users-without-explicit-perms/after.json +97 -0
  82. src_auth_perms_sync-0.5.0/tests/e2e/fixtures/set-users-without-explicit-perms/before.json +94 -0
  83. src_auth_perms_sync-0.5.0/tests/e2e/fixtures/set-users-without-explicit-perms/maps.yaml +10 -0
  84. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/test_local_cases.py +46 -5
  85. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/integration/test_cli_entrypoint.py +25 -1
  86. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/run.py +823 -53
  87. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/setup.py +127 -75
  88. src_auth_perms_sync-0.5.0/tests/setup.yaml +56 -0
  89. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/tests.yaml +224 -72
  90. src_auth_perms_sync-0.5.0/tests/unit/test_backups.py +297 -0
  91. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_cli_config.py +177 -55
  92. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_command_additive.py +46 -32
  93. src_auth_perms_sync-0.5.0/tests/unit/test_command_files.py +241 -0
  94. src_auth_perms_sync-0.5.0/tests/unit/test_module_api.py +119 -0
  95. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_permissions_sourcegraph.py +61 -0
  96. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_restore.py +76 -1
  97. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_snapshot.py +90 -14
  98. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/uv.lock +24 -24
  99. src_auth_perms_sync-0.4.0/dev/engineering-requests.md +0 -270
  100. src_auth_perms_sync-0.4.0/src/src_auth_perms_sync/__init__.py +0 -11
  101. src_auth_perms_sync-0.4.0/src/src_auth_perms_sync/shared/backups.py +0 -112
  102. src_auth_perms_sync-0.4.0/tests/e2e/fixtures/invalid-missing-repos-section/before.json +0 -48
  103. src_auth_perms_sync-0.4.0/tests/e2e/fixtures/invalid-restore-wrong-schema-version/before.json +0 -48
  104. src_auth_perms_sync-0.4.0/tests/e2e/fixtures/invalid-unknown-selector-field/before.json +0 -48
  105. src_auth_perms_sync-0.4.0/tests/e2e/fixtures/no-match-noop/before.json +0 -48
  106. src_auth_perms_sync-0.4.0/tests/unit/test_backups.py +0 -83
  107. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.env.example +0 -0
  108. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.github/CODEOWNERS +0 -0
  109. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.github/workflows/ci.yml +0 -0
  110. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.github/workflows/release.yml +0 -0
  111. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.github/workflows/validate.yml +0 -0
  112. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.markdownlint-cli2.yaml +0 -0
  113. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/.python-version +0 -0
  114. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/LICENSE +0 -0
  115. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/SECURITY.md +0 -0
  116. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/audit-dead-code.md +0 -0
  117. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/hooks/pre-commit +0 -0
  118. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/memory-analysis/mapping-efficiency.md +0 -0
  119. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/memory-analysis/memory-efficiency-analyze.py +0 -0
  120. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/memory-analysis/memory-efficiency-generate.py +0 -0
  121. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/memory-analysis/memory-efficiency-monitor-sourcegraph.sh +0 -0
  122. {src_auth_perms_sync-0.4.0/dev → src_auth_perms_sync-0.5.0/dev/memory-analysis}/memory-efficiency.md +0 -0
  123. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/dev/update-python-versions.md +0 -0
  124. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/maps-example.yaml +0 -0
  125. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/renovate.json +0 -0
  126. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/__main__.py +0 -0
  127. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/orgs/__init__.py +0 -0
  128. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/orgs/command.py +0 -0
  129. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/orgs/queries.py +0 -0
  130. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/orgs/types.py +0 -0
  131. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/__init__.py +0 -0
  132. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/apply.py +0 -0
  133. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/mapping.py +0 -0
  134. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/maps.py +0 -0
  135. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/permissions/types.py +0 -0
  136. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/shared/__init__.py +0 -0
  137. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/shared/queries.py +0 -0
  138. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/shared/saml_groups.py +0 -0
  139. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/shared/site_config.py +0 -0
  140. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/shared/sourcegraph.py +0 -0
  141. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/src/src_auth_perms_sync/shared/types.py +0 -0
  142. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/__init__.py +0 -0
  143. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/__init__.py +0 -0
  144. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/add-users-by-email-and-list/maps.yaml +0 -0
  145. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/add-users-preserves-existing/maps.yaml +0 -0
  146. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/and-filters-intersect/maps.yaml +0 -0
  147. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/empty-maps-noop/maps.yaml +0 -0
  148. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/full-overwrite-dry-run/maps.yaml +0 -0
  149. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/full-overwrite-removes-stale-grant/maps.yaml +0 -0
  150. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/full-overwrite-unions/maps.yaml +0 -0
  151. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/full-overwrite-with-backup/maps.yaml +0 -0
  152. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-bad-regex/maps.yaml +0 -0
  153. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-missing-repos-section/maps.yaml +0 -0
  154. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-restore-wrong-schema-version/snapshot.json +0 -0
  155. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-created-after-date/maps.yaml +0 -0
  156. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-repos-created-after-date/maps.yaml +0 -0
  157. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-unknown-repo/maps.yaml +0 -0
  158. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-set-unknown-user/maps.yaml +0 -0
  159. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/invalid-unknown-selector-field/maps.yaml +0 -0
  160. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/match-provider-and-host-fields/maps.yaml +0 -0
  161. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/no-match-noop/maps.yaml +0 -0
  162. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/regex-filters-scope/maps.yaml +0 -0
  163. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/saml-group-filter/maps.yaml +0 -0
  164. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/saml-group-live/maps.yaml +0 -0
  165. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-created-after-sync-saml-orgs-dry-run/maps.yaml +0 -0
  166. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-created-after-temp-user/maps.yaml +0 -0
  167. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-full-sync-saml-orgs-dry-run/maps.yaml +0 -0
  168. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-created-after/maps.yaml +0 -0
  169. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-created-after-noop/maps.yaml +0 -0
  170. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-created-after-sync-saml-orgs-dry-run/maps.yaml +0 -0
  171. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-filter/maps.yaml +0 -0
  172. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-sync-saml-orgs-dry-run/maps.yaml +0 -0
  173. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-without-explicit-perms/maps.yaml +0 -0
  174. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-repos-without-perms-sync-saml-orgs-dry-run/maps.yaml +0 -0
  175. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-users-created-after/maps.yaml +0 -0
  176. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-users-created-after-noop/maps.yaml +0 -0
  177. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-users-sync-saml-orgs-dry-run/maps.yaml +0 -0
  178. {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/set-users-without-explicit-perms → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/set-users-without-explicit-perms-default-batch}/after.json +0 -0
  179. {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/set-users-without-explicit-perms → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/set-users-without-explicit-perms-default-batch}/before.json +0 -0
  180. {src_auth_perms_sync-0.4.0/tests/e2e/fixtures/set-users-without-explicit-perms → src_auth_perms_sync-0.5.0/tests/e2e/fixtures/set-users-without-explicit-perms-default-batch}/maps.yaml +0 -0
  181. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/e2e/fixtures/set-users-without-perms-sync-saml-orgs-dry-run/maps.yaml +0 -0
  182. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/integration/__init__.py +0 -0
  183. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/__init__.py +0 -0
  184. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_apply.py +0 -0
  185. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_maps.py +0 -0
  186. {src_auth_perms_sync-0.4.0 → src_auth_perms_sync-0.5.0}/tests/unit/test_saml_groups.py +0 -0
@@ -25,5 +25,4 @@ wheels/
25
25
  !.env.example
26
26
  !.markdownlint-cli2.yaml
27
27
  !maps-example.yaml
28
- !tests/tests.yaml
29
- !tests/e2e/fixtures/**/maps.yaml
28
+ !tests/**/*.yaml
@@ -201,10 +201,16 @@ organization sync maps SAML groups to Sourcegraph org membership. Read
201
201
  CLI lives in `src/src_auth_perms_sync/`; invoke with `uv run src-auth-perms-sync`.
202
202
  Strict pyright covers the package. Root modules are entrypoints only:
203
203
 
204
- - `cli.py` — `main()`, arg parsing, owns the CLI description.
204
+ - `cli.py` — `main()`, arg parsing, owns the CLI description. Module
205
+ wrappers (`Get`/`Set`/`Restore`/`SyncSamlOrgs`) return result dataclasses
206
+ and never install logging handlers; only `main()` runs CLI-mode logging.
205
207
  - `shared/` — cross-workflow helpers: Sourcegraph auth-provider/user list
206
208
  helpers, shared GraphQL operations and TypedDicts, site-config validation,
207
- and SAML group parsing.
209
+ and SAML group parsing. `shared/backups.py` defines `RunPaths`: every
210
+ filesystem path for one run, resolved once at the edge
211
+ (`resolve_run_paths`) and threaded explicitly — never recompute paths
212
+ from cwd or globals below the edge, and honor `run_paths.write_files`
213
+ (False under `--no-files`) before any disk write.
208
214
 
209
215
  Business workflows live in packages:
210
216
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: src-auth-perms-sync
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Set Sourcegraph permissions from authentication provider data
5
5
  Project-URL: Homepage, https://github.com/sourcegraph/src-auth-perms-sync
6
6
  Project-URL: Issues, https://github.com/sourcegraph/src-auth-perms-sync/issues
@@ -18,7 +18,7 @@ Classifier: Typing :: Typed
18
18
  Requires-Python: >=3.11
19
19
  Requires-Dist: json5>=0.14.0
20
20
  Requires-Dist: pyyaml>=6.0.3
21
- Requires-Dist: src-py-lib[otel]==0.2.1
21
+ Requires-Dist: src-py-lib[otel]==0.3.0
22
22
  Description-Content-Type: text/markdown
23
23
 
24
24
  # src-auth-perms-sync
@@ -160,14 +160,44 @@ config = src.Config(
160
160
  apply=False, # Dry run (default), set to True to make changes
161
161
  )
162
162
 
163
- succeeded = src.Set(config)
163
+ result = src.Set(config) # truthy on success; result.paths has run artifacts
164
+
165
+ # Discovery returns the auth provider and code host data in memory, so you
166
+ # can assemble mapping rules without re-parsing the generated YAML files:
167
+ get_result = src.Get(config)
168
+ for provider in get_result.auth_providers:
169
+ ...
170
+ for code_host in get_result.code_hosts:
171
+ ...
164
172
 
165
173
  # Other command wrappers:
166
- # succeeded = src.Get(config)
167
- # succeeded = src.Restore(config)
168
- # succeeded = src.SyncSamlOrgs(config)
174
+ # result = src.Restore(config)
175
+ # result = src.SyncSamlOrgs(config)
176
+ ```
177
+
178
+ Module mode never touches your `logging` handlers or the root logger — your
179
+ application's logging config stays in charge. To see progress messages:
180
+
181
+ ```python
182
+ import logging
183
+
184
+ logging.basicConfig(level=logging.INFO) # or your own handlers
185
+ logging.getLogger("src_auth_perms_sync").setLevel(logging.INFO)
186
+ logging.getLogger("src_py_lib").setLevel(logging.INFO)
169
187
  ```
170
188
 
189
+ To receive structured wide events programmatically, pass an event sink:
190
+
191
+ ```python
192
+ events = src.InMemoryEventSink()
193
+ src.Get(config, event_sink=events) # or src.CallbackEventSink(my_function)
194
+ ```
195
+
196
+ To run fully disk-free (no generated YAML, snapshots, or log file), set
197
+ `no_files=True`. Combined with `apply=True` this also requires
198
+ `no_backup=True`, because skipping files gives up the before/after
199
+ snapshots that make `--apply` reversible.
200
+
171
201
  ## Inputs
172
202
 
173
203
  - Environment variables (CLI), or src.Config args (Python import)
@@ -177,7 +207,12 @@ succeeded = src.Set(config)
177
207
 
178
208
  - YAML maps file
179
209
  - By default: `src-auth-perms-sync-runs/<src_endpoint>/maps.yaml`
180
- - Or pass `--maps-path ./path/to/maps.yaml`
210
+ - Or pass `--maps-path ./path/to/maps.yaml` (works for both `get` and `set`,
211
+ so the maps file can live outside the generated artifacts tree)
212
+ - `--artifacts-dir DIR` moves the whole artifacts tree (generated YAML,
213
+ snapshots, logs); the default is `./src-auth-perms-sync-runs`
214
+ - `--no-files` writes nothing to disk; with `--apply` it also requires
215
+ `--no-backup`
181
216
  - A list of mapping rules
182
217
  - Each mapping rule takes
183
218
  - A map of filters for users
@@ -137,14 +137,44 @@ config = src.Config(
137
137
  apply=False, # Dry run (default), set to True to make changes
138
138
  )
139
139
 
140
- succeeded = src.Set(config)
140
+ result = src.Set(config) # truthy on success; result.paths has run artifacts
141
+
142
+ # Discovery returns the auth provider and code host data in memory, so you
143
+ # can assemble mapping rules without re-parsing the generated YAML files:
144
+ get_result = src.Get(config)
145
+ for provider in get_result.auth_providers:
146
+ ...
147
+ for code_host in get_result.code_hosts:
148
+ ...
141
149
 
142
150
  # Other command wrappers:
143
- # succeeded = src.Get(config)
144
- # succeeded = src.Restore(config)
145
- # succeeded = src.SyncSamlOrgs(config)
151
+ # result = src.Restore(config)
152
+ # result = src.SyncSamlOrgs(config)
153
+ ```
154
+
155
+ Module mode never touches your `logging` handlers or the root logger — your
156
+ application's logging config stays in charge. To see progress messages:
157
+
158
+ ```python
159
+ import logging
160
+
161
+ logging.basicConfig(level=logging.INFO) # or your own handlers
162
+ logging.getLogger("src_auth_perms_sync").setLevel(logging.INFO)
163
+ logging.getLogger("src_py_lib").setLevel(logging.INFO)
146
164
  ```
147
165
 
166
+ To receive structured wide events programmatically, pass an event sink:
167
+
168
+ ```python
169
+ events = src.InMemoryEventSink()
170
+ src.Get(config, event_sink=events) # or src.CallbackEventSink(my_function)
171
+ ```
172
+
173
+ To run fully disk-free (no generated YAML, snapshots, or log file), set
174
+ `no_files=True`. Combined with `apply=True` this also requires
175
+ `no_backup=True`, because skipping files gives up the before/after
176
+ snapshots that make `--apply` reversible.
177
+
148
178
  ## Inputs
149
179
 
150
180
  - Environment variables (CLI), or src.Config args (Python import)
@@ -154,7 +184,12 @@ succeeded = src.Set(config)
154
184
 
155
185
  - YAML maps file
156
186
  - By default: `src-auth-perms-sync-runs/<src_endpoint>/maps.yaml`
157
- - Or pass `--maps-path ./path/to/maps.yaml`
187
+ - Or pass `--maps-path ./path/to/maps.yaml` (works for both `get` and `set`,
188
+ so the maps file can live outside the generated artifacts tree)
189
+ - `--artifacts-dir DIR` moves the whole artifacts tree (generated YAML,
190
+ snapshots, logs); the default is `./src-auth-perms-sync-runs`
191
+ - `--no-files` writes nothing to disk; with `--apply` it also requires
192
+ `--no-backup`
158
193
  - A list of mapping rules
159
194
  - Each mapping rule takes
160
195
  - A map of filters for users
@@ -0,0 +1,458 @@
1
+ # PLAN: file handling + observability redesign
2
+
3
+ > **Status (2026-06-12): implemented**, except Track A Phase A4
4
+ > (in-memory mapping rules for `Set`), which the plan marks optional —
5
+ > tracked in [TODO.md](./TODO.md). Shipped as src-py-lib v0.3.0 and the
6
+ > src-auth-perms-sync `refactor-logging-and-files` PR. Phases were
7
+ > compressed into one PR per repo (no ContextVar bridge phase was
8
+ > needed); everything else landed as specified.
9
+
10
+ Spans both repos (we own both, both greenfield, no external users):
11
+
12
+ - `src-py-lib` — reusable plumbing (logging, events, OTel, HTTP, config)
13
+ - `src-auth-perms-sync` — consumer; runs as a CLI and as an importable
14
+ Python module (`src.Get(config)` etc.)
15
+
16
+ Two tracks. Track A fixes how files are handled. Track B fixes how logs and
17
+ structured events are handled. They meet where the JSONL event sink takes its
18
+ path from `RunPaths`.
19
+
20
+ ## Goals
21
+
22
+ 1. Module-import customers get data back in memory, control over (or freedom
23
+ from) disk writes, and their own `logging` config left untouched.
24
+ 2. CLI customers get explicit control over where files land, with default
25
+ paths unchanged.
26
+ 3. Structured events become first-class, OTel-standard wide events instead of
27
+ stowaways on the stdlib logging pipe.
28
+ 4. Net code reduction: delete the path ContextVars, the logging demux filters,
29
+ and the magic record attributes.
30
+
31
+ ## Current state (verified in code, this thread)
32
+
33
+ ### Files written today
34
+
35
+ All output roots at `Path.cwd()/src-auth-perms-sync-runs/<endpoint>/` via
36
+ `backups.endpoint_artifacts_directory()`. Three species share that tree:
37
+
38
+ | Species | Files | Nature |
39
+ | --- | --- | --- |
40
+ | Human-edited input | `maps.yaml` | `get` creates empty if missing; `set` reads |
41
+ | Regenerated reference | `code-hosts.yaml`, `auth-providers.yaml` | overwritten by every `get` |
42
+ | Append-only audit | `runs/<ts>-<cmd>/{before,after,diff}.json` + copies | gate reversibility |
43
+
44
+ The audit run directory also carries a `maps.yaml` copy and `log.json`.
45
+
46
+ Path plumbing: `cli.py:_run_or_raise` computes a run directory, then sets two
47
+ ContextVars (`backups.run_artifacts_context`). Deep code in
48
+ `permissions/restore.py`, `permissions/full_set.py`, `orgs/sync.py` calls
49
+ `backups.backup_timestamp()` / `backups.backup_path(name, ts, endpoint, cmd,
50
+ state)`, which read the ContextVars or silently recompute from cwd.
51
+
52
+ ### What `--no-backup` does and does not do
53
+
54
+ `--no-backup` skips only the reversibility artifacts (before/after snapshots,
55
+ maps.yaml backup copy). Even with it set, the tool still writes:
56
+
57
+ - `maps.yaml` creation (`run_get`, unconditional)
58
+ - `code-hosts.yaml` / `auth-providers.yaml` (`cmd_get`, unconditional)
59
+ - `log.json` (`_run_or_raise` always passes a log file path)
60
+ - dry-run snapshots/diffs — `full_set.py` gates on
61
+ `if not (dry_run or do_backup)`, so dry runs write regardless, because the
62
+ diff is the dry run's output
63
+
64
+ ### Logging today
65
+
66
+ - Two record species ride one stdlib logging pipe: human messages
67
+ (`log.info(...)` on module loggers) and structured events
68
+ (`span()`/`log_event()`), the latter with message `event=%s` and the payload
69
+ smuggled in record attrs `_src_py_lib_structured_event` /
70
+ `_src_py_lib_structured_fields`, emitted on the ROOT logger
71
+ (`logger_name=""` default).
72
+ - Demux at sinks: terminal handler carries `_DropStructuredEvents`;
73
+ `JSONLogFileHandler` knows the magic attrs and renders both species into
74
+ `log.json` (human records become `{"event": "log", ...}`).
75
+ - OTel spans open in parallel inside `span()`; OTel SDK is a hard dependency.
76
+
77
+ ### Bugs / hazards found during investigation
78
+
79
+ 1. **Module import nukes customer logging.** `configure_logging()` runs on
80
+ every `src.Get(config)` call and, with `logger_name=""`, clears the ROOT
81
+ logger's handlers and sets `propagate = False`. A host application's
82
+ logging config is destroyed.
83
+ 2. **Snapshot filename collision risk.** In a combined `set
84
+ --sync-saml-orgs` run, permission snapshots and org snapshots both resolve
85
+ to `before.json` in the same run directory (`backup_path` ignores
86
+ `source_name` when `state` is given).
87
+ 3. **Run-directory collision.** Timestamps are second-precision; concurrent
88
+ module calls or rapid CLI invocations can collide.
89
+ 4. **HTTP metric reset misplaced.** `reset_observability_metrics()` lives in
90
+ `configure_logging()`; skip handler config and a long-running script's
91
+ second run reports cumulative counters.
92
+ 5. **Audit events ride logger levels.** A host that sets root level WARNING
93
+ silently drops our info-level audit events.
94
+
95
+ ## Hard invariants (do not break; see AGENTS.md)
96
+
97
+ All four AGENTS.md invariants apply. The one this plan touches most is
98
+ invariant 3 — snapshots gate reversibility: `--apply` defaults to
99
+ before/after snapshots; `--no-backup` stays an explicit escape hatch, never
100
+ the default.
101
+
102
+ New corollary adopted in this plan: any flag combination that removes the
103
+ undo path must be stated explicitly twice (`--no-files` + `--apply` requires
104
+ `--no-backup`).
105
+
106
+ ---
107
+
108
+ ## Track A: file handling (`RunPaths`)
109
+
110
+ ### Design decisions
111
+
112
+ - **One knob**: `artifacts_dir` Config field, CLI `--artifacts-dir`, env
113
+ `SRC_AUTH_PERMS_SYNC_ARTIFACTS_DIR`. Semantics: the directory that contains
114
+ endpoint subdirectories. Default `./src-auth-perms-sync-runs` — existing
115
+ default paths do not move. Resolve to absolute once at startup
116
+ (`Path.resolve(strict=False)`) so later `os.chdir()` in a host script
117
+ cannot redirect writes. No XDG / platformdirs: these are audit artifacts in
118
+ secured offline environments; explicit operator paths beat OS conventions.
119
+ - **One value object**: frozen `RunPaths`, built once at the edge
120
+ (`_run_or_raise`) by `resolve_run_paths(config, endpoint, command)`:
121
+
122
+ ```python
123
+ @dataclass(frozen=True)
124
+ class RunPaths:
125
+ timestamp: str
126
+ artifacts_dir: Path # resolved artifacts root
127
+ endpoint_directory: Path # artifacts_dir / <endpoint-dirname>
128
+ maps_path: Path # respects --maps-path override
129
+ code_hosts_path: Path
130
+ auth_providers_path: Path
131
+ run_directory: Path # endpoint_directory / runs / <ts>-<artifact-name>
132
+ log_path: Path
133
+ ```
134
+
135
+ Threaded explicitly down `run_command` → `cmd_*` → workflow helpers. Deep
136
+ code receives concrete `Path`s; nothing recomputes from
137
+ `(timestamp, endpoint, command)`.
138
+ - **`maps.yaml` default stays endpoint-scoped.** Endpoint scoping prevents
139
+ applying one instance's mappings to another; a silent default move creates
140
+ two plausible maps files. Instead make `--maps-path` symmetric: `get` gains
141
+ the flag (today only `set` has it), so `get --maps-path ./maps.yaml`
142
+ creates/leaves that file and `set --maps-path ./maps.yaml` reads it.
143
+ - **Run directories are created exclusively**; on collision append a numeric
144
+ suffix (`...-set-apply-2`). Never overwrite run artifacts.
145
+ - **Artifact families are named.** Permission snapshots keep `before.json` /
146
+ `after.json` / `diff.json`; org-sync snapshots become
147
+ `saml-organizations-before.json` etc., so combined runs cannot collide.
148
+ - **Writability preflight.** Apply-mode commands resolve paths, create the
149
+ run directory, and verify the snapshot destination is writable BEFORE the
150
+ first mutation. Never mutate first and discover backups can't be written.
151
+ - **`no_files` knob** (CLI `--no-files`, Config field): suppress all disk
152
+ output. Distinct axis from `no_backup` (reversibility) — keep both flags.
153
+ - Guardrail 1: `no_files=True` + `apply=True` is a config error unless
154
+ `no_backup=True` is also set (give up the undo button explicitly, twice).
155
+ - Guardrail 2: `no_files` governs output only; `set` still reads its
156
+ `maps_path` input (until Track A Phase 4 in-memory maps).
157
+ - `no_files` implies `log_file=None` (lib already supports no file handler)
158
+ and no JSONL event sink.
159
+ - For CLI dry runs `--no-files` discards `diff.json` — operator's explicit
160
+ choice; the real audience is module customers, but offer uniformly.
161
+ - **Result objects.** Command wrappers return dataclasses; `__bool__` returns
162
+ `succeeded` so existing `if src.Get(config):` keeps working. Note
163
+ `__bool__` is only partial compat (`Get(c) is True` breaks); acceptable —
164
+ no external users yet. `GetResult` carries the same dicts dumped to YAML
165
+ (`cmd_get` already has them in memory; thread out via
166
+ `run_context.CommandData`):
167
+
168
+ ```python
169
+ @dataclass(frozen=True)
170
+ class GetResult:
171
+ succeeded: bool
172
+ auth_providers: list[dict[str, Any]] # as written to auth-providers.yaml
173
+ code_hosts: list[dict[str, Any]] # as written to code-hosts.yaml
174
+ maps_path: Path
175
+ maps_created: bool
176
+ paths: RunPaths | None
177
+
178
+ def __bool__(self) -> bool: ...
179
+ ```
180
+
181
+ Same pattern for `SetResult` (planned/applied diff), `RestoreResult`,
182
+ `SyncSamlOrgsResult`. Export the path helpers (`default_maps_path`,
183
+ `endpoint_artifacts_directory`) from the package `__init__`.
184
+
185
+ ### Track A phases (each ships independently)
186
+
187
+ - **A0 — characterization tests first.** Pin current behavior before
188
+ touching paths: default `get` output locations; default `set` maps path;
189
+ relative `--maps-path` is cwd-relative; `--no-backup` opt-in only; apply
190
+ fails before mutation when artifact paths are unwritable; combined
191
+ `set --sync-saml-orgs` snapshot naming (exposes the collision).
192
+ - **A1 — `artifacts_dir` + `RunPaths` at the edge.** Add the config field,
193
+ `resolve_run_paths()`, exclusive run-dir creation, `no_files` knob with
194
+ guardrails, `--maps-path` on `get`. Keep the two ContextVars as a
195
+ temporary bridge (`run_artifacts_context(paths.run_directory, ...)`). QoL
196
+ lands immediately; refactor risk stays low.
197
+ - **A2 — thread `RunPaths` down; delete the ContextVars.** Change
198
+ signatures: `run_command(..., paths)` → `run_get/run_set/run_restore/
199
+ run_sync_saml_organizations(..., paths)` → workflow helpers take concrete
200
+ paths (`write_maps_backup(paths)`, `write_snapshot_pair(paths, ...)`).
201
+ Apply artifact-family naming. When no deep code calls `backup_path()` /
202
+ `backup_timestamp()`, delete `run_artifacts_context`, both ContextVars,
203
+ and the cwd fallback. Net code reduction.
204
+ - **A3 — result dataclasses + exported helpers.** As specified above. CLI
205
+ ignores the result; module callers stop re-parsing YAML the library just
206
+ wrote.
207
+ - **A4 — in-memory mapping rules for `Set` (optional, later).** Module
208
+ customers pass parsed mapping rules instead of a maps file; the full
209
+ get → assemble → dry-run set loop never touches disk. Snapshots still go
210
+ to disk on `--apply` unless `no_files` + `no_backup` are both explicit.
211
+
212
+ ---
213
+
214
+ ## Track B: observability (`EventSink` + OTel wide events)
215
+
216
+ ### Design decisions: three concerns, three channels
217
+
218
+ ```text
219
+ Human messages Structured events Tracing
220
+ log.info("Wrote %s") span() / log_event() OTel spans
221
+ │ │ │
222
+ ▼ ▼ │
223
+ stdlib logging EventSink (explicit, per run) │
224
+ module loggers only ├ JSONLEventSink → log.json │
225
+ │ ├ InMemoryEventSink (tests, │
226
+ │ │ module customers) │
227
+ │ ├ CallbackEventSink │
228
+ │ ├ OtelLogsSink (--otel) │
229
+ │ └ NullEventSink (default) │
230
+ CLI mode only: ▲ │
231
+ terminal handler EventBridgeHandler forwards human │
232
+ on package loggers, log records INTO the sink so │
233
+ never root log.json keeps everything │
234
+ ```
235
+
236
+ - **Bridge direction reversed.** Today rich events are flattened onto the
237
+ logging pipe (magic attrs) and unpacked at the file handler. Instead:
238
+ events go to the sink directly as dicts; ONE `EventBridgeHandler`
239
+ (CLI-installed) wraps lossy human log records into `{"event_name": "log"}`
240
+ events. The adapter direction matches data richness.
241
+ - **Entrypoint is the mode signal — no `manage_logging` knob.**
242
+ `main()` runs CLI mode (terminal handler + bridge + JSONL sink, handlers
243
+ attached only to `("src_auth_perms_sync", "src_py_lib")`, never root, no
244
+ `handlers.clear()`, no `propagate` changes on host loggers).
245
+ `Get/Set/Restore/SyncSamlOrgs` run guest mode: no handler changes
246
+ anywhere, `NullEventSink` default, optional caller-provided sink. Standard
247
+ etiquette `logging.NullHandler()` on the package loggers.
248
+ - **Sink resolution via ContextVar (`EventRuntime`).** One deliberate
249
+ exception to the "kill ContextVars" rule: the event runtime
250
+ (`run`, `sink`, `min_level`, `base_fields`) is genuinely ambient execution
251
+ context, like the current OTel span. Guardrails: only
252
+ `observability_context()` sets it; default is `NullEventSink`; thread
253
+ pools keep using `contextvars.copy_context()` via
254
+ `submit_with_log_context`; no import-time side effects.
255
+ (`RunPaths` stays explicitly threaded — paths are not ambient.)
256
+ - **`span()` / `log_event()` / `event()` / `stage()` signatures unchanged.**
257
+ Only the backend moves; consumer call sites in src-auth-perms-sync are
258
+ untouched.
259
+ - **Split `logging_context()`** (currently does too much) into:
260
+ - `observability_context(name, *, sink, run, run_fields, open_telemetry,
261
+ resource_sample_interval_seconds, ...)` — sets the EventRuntime, OTel
262
+ only if explicitly requested, run start/end events, resource sampler,
263
+ metric reset (moved here from `configure_logging`), sink lifecycle.
264
+ - `cli_logging_handlers(*, sink, run, logger_names, terminal_level,
265
+ audit_log_level)` — adds/removes only its own handlers.
266
+ - **Teardown ordering** (baked into one context manager): stop resource
267
+ sampler → wait for worker threads → emit run-end event → flush/close JSONL
268
+ sink → force-flush OTel → remove CLI handlers. Run-end event must land
269
+ even on `SystemExit` / exceptions; OTel flush failure must not prevent
270
+ JSON flush; no logging from inside sink error handling (recursion).
271
+ - **OTel global provider hygiene.** Never configure OTel in guest mode
272
+ unless the caller asks; if the host already configured OTel, `span()`
273
+ uses it. Add an `owned` flag to `OpenTelemetryRuntime`; only force-flush
274
+ providers this run configured.
275
+ - **Level policy.** Audit completeness is governed by the sink, not logger
276
+ levels: `log.json` defaults to complete (debug); `--quiet` affects
277
+ terminal only. Lifecycle events (run start/end, startup config snapshot,
278
+ errors) are always emitted.
279
+ - **Thread safety.** JSONL sink locks around writes; copy event dicts before
280
+ customer callbacks; worker pools complete inside the observability
281
+ context; document that `ProcessPoolExecutor` does not inherit context.
282
+
283
+ ### OTel standards adoption (decided inside B1/B2, not a separate phase)
284
+
285
+ - **Event schema = OTel Logs Data Model field names:**
286
+
287
+ | Today | OTel standard |
288
+ | --- | --- |
289
+ | `ts` (string) | `time_unix_nano` |
290
+ | `level` | `severity_text` + `severity_number` (INFO=9, WARN=13, ERROR=17) |
291
+ | `event` key | `event_name` |
292
+ | `message` | `body` |
293
+ | `trace` / `span` / `parent_span` | `trace_id` / `span_id` (hex from our OTel spans) |
294
+ | other fields | `attributes` |
295
+ | run-level fields | `resource` attributes, stamped once per run |
296
+
297
+ - **Semantic conventions for attribute names**, pinned in one
298
+ `semconv.py` constants module (semconv is versioned and partly
299
+ experimental; an upstream rename must be a one-file change):
300
+ `error_type` → `error.type`; pid → `process.pid`; Python version →
301
+ `process.runtime.version`; `service.name` / `service.version`; HTTP
302
+ run-summary counters aligned to `http.client.*`. App-specific fields get
303
+ our own namespace so they can never collide: `sync.command`,
304
+ `sync.endpoint`, `sync.run.id`, `sync.parallelism`, etc.
305
+ - **Wide-event discipline: one event per unit of work.** The span-END event
306
+ is THE wide event (the codebase already accumulates attributes onto
307
+ `cmd_event[...]` during work — keep that pattern). Demote span-START
308
+ events to debug; demote run-start once `startup_event` carries the config
309
+ snapshot. `log.json` gets ~half the lines and every line is worth reading.
310
+ - **File format: flat JSONL with OTel field names** (human-greppable,
311
+ audit-friendly, one trivial transform from OTLP). NOT per-line OTLP/JSON
312
+ envelopes (`resourceLogs[...]` — three envelopes deep, miserable to audit
313
+ by eye). True-OTLP interop is a separate sink: when `--otel` is on,
314
+ `OtelLogsSink` emits wide events through the OTel Logs SDK
315
+ (`LoggerProvider` → OTLP/HTTP) — already a dependency. Offline customers
316
+ keep the readable file; connected ones get standard wire logs correlated
317
+ with our traces via `trace_id`/`span_id`.
318
+
319
+ ### Custom logic carry-over inventory (audited 2026-06-12)
320
+
321
+ Existing custom logic in `src_py_lib/utils/logging.py` and its fate. Most
322
+ survives because `span()` / `log_event()` / context machinery keep their
323
+ signatures; four items live in code Track B deletes and MUST be carried over
324
+ explicitly:
325
+
326
+ 1. **HTTP header secret redaction** (`_http_headers` +
327
+ `SECRET_FIELD_FRAGMENTS`): scrubs authorization/cookie/token headers
328
+ before they reach `log.json`. Lives in the handler path B6 deletes. Move
329
+ the redaction (and `_is_sensitive_log_field`) into the bridge/mining path.
330
+ Losing this writes bearer tokens to disk during wire debugging.
331
+ 2. **httpcore/httpx wire-debug mining** (`_structured_log_fields`):
332
+ `ast.literal_eval`s httpcore debug messages into structured
333
+ `status_code` / `http_version` / redacted-header fields; demotes httpx
334
+ "HTTP Request:" lines to debug. The bridge attaches to package loggers
335
+ only, so httpx/httpcore records never reach the sink — the opt-in
336
+ wire-debug mode (`suppress_http_dependency_logs=False`) would silently
337
+ die. Carry-over: when suppression is disabled, also attach the bridge to
338
+ `("httpx", "httpcore")` and keep the mining + redaction in the bridge.
339
+ 3. **Log-file retention** (`_prune_old_log_files`, `retain_log_files`):
340
+ inert for src-auth-perms-sync (per-run directories don't match the
341
+ prune glob) but active for other src-py-lib consumers using the default
342
+ `logs/` dir. Keep pruning where the sink's file is created
343
+ (`logs_dir`-style construction helper), not in the sink itself.
344
+ 4. **`exc_info` traceback formatting and field ordering**
345
+ (`_ordered_payload`, `LOG_FIELD_ORDER`): move traceback rendering into
346
+ `EventBridgeHandler`; move ordering into the JSONL sink, updated to the
347
+ OTel field names (`time_unix_nano`, `severity_text`, `event_name`,
348
+ `trace_id`, `span_id` first, attributes alphabetical).
349
+
350
+ Verified safe without action (mechanism unchanged by the plan):
351
+ `log_context`/`stage` inherited fields; `submit_with_log_context`;
352
+ trace-field stamping; HTTP metric counters wired into `HTTPClient`;
353
+ `ResourceSampler` (emits via `log_event`, so it follows the sink);
354
+ `startup_event` git hash + `sanitized_config_snapshot` (incl. `op://` →
355
+ "reference" secret-state detection); run-end `SystemExit(0)`/exit-code
356
+ semantics; `-v/-q/-s` alias validation and level parsing; OTel span
357
+ attributes/status and traceparent propagation for `fetch_sg_traces`.
358
+
359
+ `_DropHTTPDependencyLogs` on the terminal handler becomes unnecessary by
360
+ construction (handlers sit on package loggers, so httpx/httpcore noise
361
+ never reaches them) — delete it; behavior is preserved.
362
+
363
+ ### Track B steps (lib first, consumer second)
364
+
365
+ - **B1 — sinks beside the old code (src-py-lib).** `EventSink` protocol;
366
+ `NullEventSink`, `JSONLEventSink` (locked writes, flush/close),
367
+ `CompositeEventSink`, `InMemoryEventSink`, `CallbackEventSink`;
368
+ `EventRuntime` ContextVar; `semconv.py`; tests for JSONL output shape
369
+ (OTel field names, severity numbers, resource stamping).
370
+ - **B2 — move `log_event()` off stdlib logging (src-py-lib).** Build the
371
+ OTel-shaped payload, emit to the current sink, keep OTel span-event
372
+ emission; delete the `LogRecord.extra` magic-attr path. Apply wide-event
373
+ discipline (start events → debug).
374
+ - **B3 — `EventBridgeHandler` (src-py-lib).** Human records → sink as
375
+ `event_name="log"` events. CLI-installed only, attached to
376
+ `("src_auth_perms_sync", "src_py_lib")`.
377
+ - **B4 — split `logging_context()` (src-py-lib).** Into
378
+ `observability_context()` and `cli_logging_handlers()` as above; move
379
+ `reset_observability_metrics()` into `observability_context()`; OTel
380
+ `owned` flag.
381
+ - **B5 — consumer wiring (src-auth-perms-sync).** `_run_or_raise(...,
382
+ cli_mode: bool)`; `main()` passes `cli_mode=True` (JSONL sink at
383
+ `RunPaths.log_path`, terminal + bridge handlers); `Get/Set/Restore/
384
+ SyncSamlOrgs` pass `cli_mode=False` (guest mode; optional `event_sink`
385
+ parameter for customers who want events programmatically). README gains a
386
+ module-logging snippet (root WARNING default means a long sync looks
387
+ silent — show `logging.getLogger("src_auth_perms_sync")` setup).
388
+ - **B6 — delete the old demux (src-py-lib).** Remove
389
+ `_DropStructuredEvents`, `_STRUCTURED_EVENT_ATTR`,
390
+ `_STRUCTURED_FIELDS_ATTR`, the structured branch in `JSONLogFileHandler`
391
+ (or the whole handler, replaced by the sink), and the destructive root
392
+ `handlers.clear()`. Regression tests: CLI produces `log.json`;
393
+ `log.info()` appears as `event_name="log"`; structured events appear with
394
+ full attributes; module `Get(config)` does not mutate root handlers or
395
+ levels; thread-pool events retain run/context fields; run-end event
396
+ appears on exceptions; second module run in one process reports per-run
397
+ (not cumulative) HTTP counters.
398
+
399
+ ---
400
+
401
+ ## Track interaction points
402
+
403
+ - `JSONLEventSink` path comes from `RunPaths.log_path` (A1 must land before
404
+ B5's consumer wiring, or B5 temporarily uses the current log path helper).
405
+ - `no_files` (A1) disables the JSONL sink and the log file; guest mode (B5)
406
+ defaults to no file output anyway — `no_files` mainly matters for CLI and
407
+ for module callers who set `artifacts_dir`.
408
+ - Result dataclasses (A3) and `InMemoryEventSink` / `CallbackEventSink` (B1)
409
+ together complete the module story: data back in memory, events back in
410
+ memory, zero disk unless asked (and snapshots on `--apply`, always, unless
411
+ doubly opted out).
412
+
413
+ ## Verification
414
+
415
+ - `uv run tests/run.py` (lint, format, pyright, unit + fixture tests, CLI
416
+ rejection matrix, randomized permission invariants) after every phase.
417
+ - `uv run tests/run.py --live` for mutation-path phases (A2 especially), with
418
+ before/after snapshot inspection under `src-auth-perms-sync-runs/`.
419
+ - A0 characterization tests are the safety net for every later phase: default
420
+ paths must not move.
421
+ - New fixture cases for: `--artifacts-dir`, `get --maps-path`, `--no-files`
422
+ rejection matrix (`--no-files --apply` without `--no-backup` is an error),
423
+ combined-run snapshot naming, log.json schema goldens.
424
+ - Per AGENTS.md: dry-run against the .env test instance and read the planned
425
+ changes before any `--apply`; inspect snapshots afterward.
426
+
427
+ ## Explicitly out of scope (revisit triggers noted)
428
+
429
+ - **Moving the `maps.yaml` default** out of the artifacts tree — revisit only
430
+ if compatibility constraints loosen; `--maps-path` symmetry covers the need.
431
+ - **XDG / platformdirs** — explicit flag + env is correct for secured offline
432
+ environments.
433
+ - **`QueueEventSink` / async writer thread** — only if event volume, slow
434
+ customer callbacks, or OTLP backpressure measurably hurt sync runs.
435
+ - **Artifact manifest / `ArtifactStore` abstraction** — only if artifact
436
+ families multiply beyond the current three.
437
+ - **Strict per-line OTLP/JSON file format** — only if a customer needs
438
+ collector `otlpjsonfile` ingestion of `log.json` as-is; ship as a sink
439
+ variant, not a default.
440
+
441
+ ## Sequencing summary
442
+
443
+ ```text
444
+ A0 characterization tests
445
+ A1 artifacts_dir + RunPaths (bridge) + no_files + get --maps-path
446
+ B1 sinks + OTel schema + semconv (src-py-lib, parallel with A1/A2)
447
+ B2 log_event → sink, wide events
448
+ A2 thread RunPaths, delete ContextVars
449
+ B3 bridge handler
450
+ B4 observability_context split
451
+ B5 consumer cli_mode wiring (needs A1 for RunPaths.log_path)
452
+ A3 result dataclasses + exports
453
+ B6 delete demux + regression tests
454
+ A4 in-memory maps for Set (optional, last)
455
+ ```
456
+
457
+ Each step lands as its own PR from a clean worktree off `origin/main`, lint
458
+ and tests green, docs updated.
@@ -1,21 +1,14 @@
1
1
  # TODO
2
2
 
3
- ## Medium priority: extend SAML-group live coverage to org sync
3
+ ## Follow-up: in-memory mapping rules for Set (PLAN.md Track A Phase A4)
4
4
 
5
- tests/setup.py now fabricates SAML accounts with synthetic groups
6
- (`perms-sync-test-eng` / `perms-sync-test-sales`, see tests/setup.yaml).
7
- saml-group-live covers permission mapping; add a seeded
8
- `sync-saml-orgs --apply` live case that maps those groups to a throwaway
9
- org and asserts membership is added AND removed (today's
10
- sync-saml-orgs-apply only covers the single real Okta user, add-only).
11
-
12
- ## Decide: pendingBindIDs / usersWithPendingPermissions
13
-
14
- The CLI cannot create pending permissions (it validates users exist), but
15
- snapshots record `pending_bindIDs`, and setup.py / the live hygiene check
16
- report (never delete) any that appear. Decide whether "grant before first
17
- login" is a customer need; if not, consider dropping the snapshot field.
18
- See the thread discussion 2026-06-11.
5
+ The rest of [PLAN.md](./PLAN.md) is implemented (src-py-lib v0.3.0 +
6
+ the consumer refactor-logging-and-files PR). The one deliberately
7
+ deferred piece, marked optional in the plan: let module callers pass
8
+ parsed mapping rules to `Set` instead of a maps file, so the full
9
+ get → assemble → dry-run loop never touches disk. Snapshots must stay
10
+ on disk for `--apply` (reversibility invariant); `no_files` + `apply`
11
+ must keep requiring `no_backup`.
19
12
 
20
13
  ## High priority: Remote trigger on demand
21
14
 
@@ -30,12 +23,7 @@ See the thread discussion 2026-06-11.
30
23
  - How do we avoid stampedes (e.g., bulk repo sync triggering thousands
31
24
  of re-runs)?
32
25
 
33
- ## High priority: Verify perms are updated when a user's SAML groups change
34
-
35
- - If a user gets added to a new SAML group, which hits a mapping, ensure they
36
- get the new perms
37
-
38
- ## High priority: Reduce worst-case full-permission sync load
26
+ ## Medium priority: Reduce worst-case full-permission sync load
39
27
 
40
28
  - Use the stress-run evidence in
41
29
  [engineering-requests.md](./engineering-requests.md)