nthlayer-generate 2.0.2__tar.gz → 2.0.4__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 (351) hide show
  1. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/PKG-INFO +1 -1
  2. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/pyproject.toml +1 -1
  3. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/__init__.py +1 -1
  4. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/init.py +202 -30
  5. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/setup.py +165 -39
  6. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/manifest.py +57 -2
  7. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/validator.py +6 -5
  8. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate.egg-info/PKG-INFO +1 -1
  9. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate.egg-info/SOURCES.txt +1 -0
  10. nthlayer_generate-2.0.4/tests/test_cli_setup.py +1788 -0
  11. nthlayer_generate-2.0.4/tests/test_init.py +1729 -0
  12. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_init_documented_flags.py +6 -0
  13. nthlayer_generate-2.0.4/tests/test_source_citations.py +117 -0
  14. nthlayer_generate-2.0.2/tests/test_cli_setup.py +0 -944
  15. nthlayer_generate-2.0.2/tests/test_init.py +0 -621
  16. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/README.md +0 -0
  17. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/setup.cfg +0 -0
  18. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/alertmanager/__init__.py +0 -0
  19. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/alertmanager/config.py +0 -0
  20. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/alerts/__init__.py +0 -0
  21. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/alerts/loader.py +0 -0
  22. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/alerts/models.py +0 -0
  23. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/alerts/validator.py +0 -0
  24. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/__init__.py +0 -0
  25. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/alerts.py +0 -0
  26. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/apply.py +0 -0
  27. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/backstage.py +0 -0
  28. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/dashboard.py +0 -0
  29. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/dashboard_validate.py +0 -0
  30. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/dependencies.py +0 -0
  31. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/docs.py +0 -0
  32. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/environments.py +0 -0
  33. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/formatters/__init__.py +0 -0
  34. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/formatters/json_fmt.py +0 -0
  35. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/formatters/junit.py +0 -0
  36. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/formatters/markdown.py +0 -0
  37. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/formatters/models.py +0 -0
  38. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/formatters/sarif.py +0 -0
  39. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/generate.py +0 -0
  40. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/generate_alerts.py +0 -0
  41. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/generate_loki.py +0 -0
  42. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/identity.py +0 -0
  43. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/lint.py +0 -0
  44. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/migrate.py +0 -0
  45. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/migrate_manifest.py +0 -0
  46. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/ownership.py +0 -0
  47. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/pagerduty.py +0 -0
  48. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/plan.py +0 -0
  49. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/recommend_metrics.py +0 -0
  50. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/recording_rules.py +0 -0
  51. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/simulate.py +0 -0
  52. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/slo.py +0 -0
  53. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/templates.py +0 -0
  54. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/topology.py +0 -0
  55. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/ux.py +0 -0
  56. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/validate.py +0 -0
  57. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/validate_metadata.py +0 -0
  58. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/validate_slo.py +0 -0
  59. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/cli/validate_spec.py +0 -0
  60. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/clients/__init__.py +0 -0
  61. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/clients/base.py +0 -0
  62. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/clients/cortex.py +0 -0
  63. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/clients/pagerduty.py +0 -0
  64. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/clients/slack.py +0 -0
  65. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/config/__init__.py +0 -0
  66. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/config/cli.py +0 -0
  67. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/config/integrations.py +0 -0
  68. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/config/loader.py +0 -0
  69. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/config/secrets/__init__.py +0 -0
  70. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/config/secrets/backends.py +0 -0
  71. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/config/settings.py +0 -0
  72. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/core/__init__.py +0 -0
  73. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/core/errors.py +0 -0
  74. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/core/tiers.py +0 -0
  75. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/__init__.py +0 -0
  76. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/builder_sdk.py +0 -0
  77. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/intents.py +0 -0
  78. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/manifest_builder.py +0 -0
  79. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/models.py +0 -0
  80. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/panel_spec.py +0 -0
  81. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/resolver.py +0 -0
  82. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/sdk_adapter.py +0 -0
  83. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/__init__.py +0 -0
  84. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/base.py +0 -0
  85. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/base_intent.py +0 -0
  86. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/consul_intent.py +0 -0
  87. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/elasticsearch_intent.py +0 -0
  88. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/etcd_intent.py +0 -0
  89. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/haproxy_intent.py +0 -0
  90. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/http_intent.py +0 -0
  91. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/kafka_intent.py +0 -0
  92. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/kubernetes.py +0 -0
  93. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/mongodb_intent.py +0 -0
  94. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/mysql_intent.py +0 -0
  95. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/nats_intent.py +0 -0
  96. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/nginx_intent.py +0 -0
  97. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/postgresql_intent.py +0 -0
  98. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/pulsar_intent.py +0 -0
  99. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/rabbitmq_intent.py +0 -0
  100. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/redis_intent.py +0 -0
  101. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/stream_intent.py +0 -0
  102. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/traefik_intent.py +0 -0
  103. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/templates/worker_intent.py +0 -0
  104. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dashboards/validator.py +0 -0
  105. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/demo.py +0 -0
  106. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dependencies/__init__.py +0 -0
  107. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dependencies/discovery.py +0 -0
  108. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dependencies/models.py +0 -0
  109. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dependencies/providers/__init__.py +0 -0
  110. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dependencies/providers/backstage.py +0 -0
  111. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dependencies/providers/base.py +0 -0
  112. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dependencies/providers/consul.py +0 -0
  113. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dependencies/providers/etcd.py +0 -0
  114. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dependencies/providers/kubernetes.py +0 -0
  115. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dependencies/providers/prometheus.py +0 -0
  116. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/dependencies/providers/zookeeper.py +0 -0
  117. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/discovery/__init__.py +0 -0
  118. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/discovery/classifier.py +0 -0
  119. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/discovery/client.py +0 -0
  120. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/discovery/models.py +0 -0
  121. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/domain/__init__.py +0 -0
  122. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/domain/models.py +0 -0
  123. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/generators/__init__.py +0 -0
  124. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/generators/alerts.py +0 -0
  125. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/generators/backstage.py +0 -0
  126. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/generators/docs.py +0 -0
  127. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/generators/sloth.py +0 -0
  128. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/identity/__init__.py +0 -0
  129. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/identity/models.py +0 -0
  130. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/identity/normalizer.py +0 -0
  131. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/identity/ownership.py +0 -0
  132. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/identity/ownership_providers/__init__.py +0 -0
  133. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/identity/ownership_providers/backstage.py +0 -0
  134. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/identity/ownership_providers/base.py +0 -0
  135. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/identity/ownership_providers/codeowners.py +0 -0
  136. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/identity/ownership_providers/declared.py +0 -0
  137. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/identity/ownership_providers/kubernetes.py +0 -0
  138. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/identity/ownership_providers/pagerduty.py +0 -0
  139. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/identity/resolver.py +0 -0
  140. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/integrations/__init__.py +0 -0
  141. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/integrations/pagerduty.py +0 -0
  142. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/logging.py +0 -0
  143. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/loki/__init__.py +0 -0
  144. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/loki/generator.py +0 -0
  145. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/loki/models.py +0 -0
  146. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/loki/templates.py +0 -0
  147. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/__init__.py +0 -0
  148. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/discovery.py +0 -0
  149. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/models.py +0 -0
  150. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/recommender.py +0 -0
  151. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/runtime/__init__.py +0 -0
  152. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/runtime/go.py +0 -0
  153. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/runtime/jvm.py +0 -0
  154. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/runtime/nodejs.py +0 -0
  155. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/runtime/python.py +0 -0
  156. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/standards/__init__.py +0 -0
  157. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/standards/aliases.py +0 -0
  158. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/standards/otel_semconv.py +0 -0
  159. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/templates/__init__.py +0 -0
  160. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/templates/api.py +0 -0
  161. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/templates/cache.py +0 -0
  162. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/templates/database_client.py +0 -0
  163. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/templates/gateway.py +0 -0
  164. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/templates/grpc.py +0 -0
  165. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/templates/queue_consumer.py +0 -0
  166. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/templates/registry.py +0 -0
  167. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/metrics/templates/worker.py +0 -0
  168. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/orchestration/__init__.py +0 -0
  169. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/orchestration/engine.py +0 -0
  170. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/orchestration/handlers.py +0 -0
  171. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/orchestration/plan_builder.py +0 -0
  172. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/orchestration/registry.py +0 -0
  173. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/orchestration/results.py +0 -0
  174. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/orchestrator.py +0 -0
  175. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/pagerduty/__init__.py +0 -0
  176. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/pagerduty/defaults.py +0 -0
  177. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/pagerduty/naming.py +0 -0
  178. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/pagerduty/orchestration.py +0 -0
  179. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/pagerduty/resources.py +0 -0
  180. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/policies/__init__.py +0 -0
  181. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/policies/engine.py +0 -0
  182. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/policies/models.py +0 -0
  183. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/policies/rules.py +0 -0
  184. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/providers/__init__.py +0 -0
  185. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/providers/__main__.py +0 -0
  186. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/providers/base.py +0 -0
  187. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/providers/cli.py +0 -0
  188. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/providers/grafana.py +0 -0
  189. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/providers/lock.py +0 -0
  190. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/providers/mimir.py +0 -0
  191. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/providers/pagerduty.py +0 -0
  192. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/providers/prometheus.py +0 -0
  193. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/providers/registry.py +0 -0
  194. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/recording_rules/__init__.py +0 -0
  195. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/recording_rules/builder.py +0 -0
  196. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/recording_rules/manifest_builder.py +0 -0
  197. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/recording_rules/models.py +0 -0
  198. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/simulate/__init__.py +0 -0
  199. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/simulate/engine.py +0 -0
  200. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/simulate/graph.py +0 -0
  201. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/simulate/models.py +0 -0
  202. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/simulate/output.py +0 -0
  203. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/simulate/what_if.py +0 -0
  204. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/slos/__init__.py +0 -0
  205. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/slos/alerts.py +0 -0
  206. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/slos/calculator.py +0 -0
  207. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/slos/ceiling.py +0 -0
  208. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/slos/dependencies.py +0 -0
  209. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/slos/models.py +0 -0
  210. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/slos/parser.py +0 -0
  211. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/slos/pipeline.py +0 -0
  212. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/__init__.py +0 -0
  213. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/alerting.py +0 -0
  214. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/builtin_templates/__init__.py +0 -0
  215. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/contracts.py +0 -0
  216. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/custom_templates.py +0 -0
  217. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/environment_alerts.py +0 -0
  218. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/environment_detection.py +0 -0
  219. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/environment_gates.py +0 -0
  220. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/environment_merger.py +0 -0
  221. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/environments.py +0 -0
  222. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/helpers.py +0 -0
  223. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/loader.py +0 -0
  224. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/models.py +0 -0
  225. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/opensrm_parser.py +0 -0
  226. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/parser.py +0 -0
  227. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/template.py +0 -0
  228. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/template_loader.py +0 -0
  229. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/templates.py +0 -0
  230. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/specs/variable_substitution.py +0 -0
  231. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/topology/__init__.py +0 -0
  232. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/topology/enrichment.py +0 -0
  233. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/topology/models.py +0 -0
  234. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/topology/serializers.py +0 -0
  235. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/user_config.py +0 -0
  236. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/validation/__init__.py +0 -0
  237. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/validation/conftest.py +0 -0
  238. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/validation/metadata.py +0 -0
  239. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/validation/promql.py +0 -0
  240. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate/validation/promruval.py +0 -0
  241. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate.egg-info/dependency_links.txt +0 -0
  242. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate.egg-info/entry_points.txt +0 -0
  243. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate.egg-info/requires.txt +0 -0
  244. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/src/nthlayer_generate.egg-info/top_level.txt +0 -0
  245. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_alert_pipeline.py +0 -0
  246. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_alerting_config.py +0 -0
  247. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_alertmanager.py +0 -0
  248. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_alerts.py +0 -0
  249. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_backstage_generator.py +0 -0
  250. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_budget_policy.py +0 -0
  251. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_alerts.py +0 -0
  252. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_apply.py +0 -0
  253. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_dashboard.py +0 -0
  254. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_dashboard_validate.py +0 -0
  255. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_environments.py +0 -0
  256. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_formatters.py +0 -0
  257. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_generate.py +0 -0
  258. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_generate_alerts.py +0 -0
  259. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_generate_loki.py +0 -0
  260. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_identity.py +0 -0
  261. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_lint.py +0 -0
  262. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_migrate_manifest.py +0 -0
  263. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_pagerduty.py +0 -0
  264. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_plan.py +0 -0
  265. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_slo.py +0 -0
  266. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_templates.py +0 -0
  267. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_ux.py +0 -0
  268. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_validate.py +0 -0
  269. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_validate_metadata.py +0 -0
  270. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_validate_slo.py +0 -0
  271. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_cli_validate_spec.py +0 -0
  272. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_clients.py +0 -0
  273. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_config_cli.py +0 -0
  274. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_config_loader.py +0 -0
  275. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_config_secrets_backends.py +0 -0
  276. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_config_secrets_init.py +0 -0
  277. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_conftest_validation.py +0 -0
  278. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_contract_validation.py +0 -0
  279. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_core_errors.py +0 -0
  280. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_core_tiers.py +0 -0
  281. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_critical_models.py +0 -0
  282. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_custom_templates.py +0 -0
  283. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_dashboard_generation.py +0 -0
  284. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_dashboard_templates.py +0 -0
  285. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_dashboards_builder_sdk.py +0 -0
  286. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_dashboards_models.py +0 -0
  287. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_dashboards_resolver.py +0 -0
  288. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_dashboards_validator.py +0 -0
  289. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_demo.py +0 -0
  290. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_demo_app_auth.py +0 -0
  291. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_dependencies.py +0 -0
  292. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_dependency_declarations.py +0 -0
  293. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_deps_discovery.py +0 -0
  294. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_deps_provider_backstage.py +0 -0
  295. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_deps_provider_consul.py +0 -0
  296. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_deps_provider_etcd.py +0 -0
  297. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_deps_provider_kubernetes.py +0 -0
  298. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_deps_provider_zookeeper.py +0 -0
  299. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_discovery_client.py +0 -0
  300. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_docs_generator.py +0 -0
  301. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_domain_models.py +0 -0
  302. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_env_flag.py +0 -0
  303. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_environment_commands.py +0 -0
  304. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_generators_alerts.py +0 -0
  305. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_generators_backstage.py +0 -0
  306. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_generators_docs.py +0 -0
  307. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_generators_sloth.py +0 -0
  308. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_hybrid_dashboard_builder.py +0 -0
  309. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_identity.py +0 -0
  310. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_integrations_pagerduty.py +0 -0
  311. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_intent_templates.py +0 -0
  312. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_loki.py +0 -0
  313. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_metadata_validation.py +0 -0
  314. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_metrics_discovery.py +0 -0
  315. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_metrics_recommender.py +0 -0
  316. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_mimir_provider.py +0 -0
  317. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_multi_environment_integration.py +0 -0
  318. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_opensrm.py +0 -0
  319. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_orchestrator.py +0 -0
  320. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_ownership.py +0 -0
  321. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_pagerduty.py +0 -0
  322. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_pagerduty_module.py +0 -0
  323. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_pagerduty_orchestration.py +0 -0
  324. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_pagerduty_resources.py +0 -0
  325. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_policy_engine.py +0 -0
  326. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_policy_handler.py +0 -0
  327. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_policy_models.py +0 -0
  328. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_policy_rules.py +0 -0
  329. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_prometheus_provider.py +0 -0
  330. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_provider_grafana.py +0 -0
  331. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_provider_pagerduty.py +0 -0
  332. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_providers_lock.py +0 -0
  333. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_recording_rules.py +0 -0
  334. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_sdk_adapter.py +0 -0
  335. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_service_type_agreement.py +0 -0
  336. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_simulate_cli.py +0 -0
  337. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_simulate_engine.py +0 -0
  338. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_simulate_graph.py +0 -0
  339. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_simulate_models.py +0 -0
  340. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_simulate_what_if.py +0 -0
  341. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_slo_alerts.py +0 -0
  342. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_slo_ceiling.py +0 -0
  343. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_slo_cli.py +0 -0
  344. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_slos.py +0 -0
  345. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_specs.py +0 -0
  346. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_sync_awesome_alerts.py +0 -0
  347. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_templates.py +0 -0
  348. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_topology.py +0 -0
  349. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_user_config.py +0 -0
  350. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_validation.py +0 -0
  351. {nthlayer_generate-2.0.2 → nthlayer_generate-2.0.4}/tests/test_validation_promruval.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nthlayer-generate
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: NthLayer - The Missing Layer of Reliability
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nthlayer-generate"
7
- version = "2.0.2"
7
+ version = "2.0.4"
8
8
  description = "NthLayer - The Missing Layer of Reliability"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "2.0.2"
5
+ __version__ = "2.0.4"
@@ -1,7 +1,9 @@
1
1
  """CLI command for initializing new NthLayer services."""
2
2
 
3
+ import re
3
4
  from pathlib import Path
4
5
 
6
+ import yaml
5
7
  from nthlayer_common.manifest.models import resolve_service_type
6
8
 
7
9
  from nthlayer_generate.cli.ux import (
@@ -17,6 +19,7 @@ from nthlayer_generate.cli.ux import (
17
19
  )
18
20
  from nthlayer_generate.core.tiers import TIER_CONFIGS
19
21
  from nthlayer_generate.specs.custom_templates import CustomTemplateLoader
22
+ from nthlayer_generate.specs.manifest import is_valid_service_name
20
23
 
21
24
  # The service-type menu. INVARIANT: every key is a value a manifest can
22
25
  # store verbatim — nothing here is translated on the way out (opensrm-8qpd).
@@ -115,12 +118,22 @@ def init_command(
115
118
  if not team and interactive:
116
119
  team = text_input("Team name", placeholder="e.g., platform, payments")
117
120
 
118
- if not team:
121
+ # `not team` first: team is `str | None` here, so `.strip()` alone would
122
+ # raise on None and also lose the narrowing the calls below rely on. The
123
+ # `.strip()` half is what catches a whitespace-only --team here, where the
124
+ # message fits it, rather than at the _is_valid_team branch below, whose
125
+ # message would be false for it.
126
+ if not team or not team.strip():
119
127
  error("Team name is required")
120
128
  if not interactive:
121
129
  console.print(" [muted]Pass it with --team <team>[/muted]")
122
130
  return 1
123
131
 
132
+ if not _is_valid_team(team):
133
+ error("Invalid team name")
134
+ console.print(" [muted]Team name must not contain line breaks, tabs or NUL[/muted]")
135
+ return 1
136
+
124
137
  # Select service tier using interactive menu
125
138
  tier = None
126
139
  if interactive:
@@ -196,10 +209,25 @@ def init_command(
196
209
  service_name, team, tier, service_type, dependencies, template_obj
197
210
  )
198
211
 
212
+ # encoding="utf-8" explicitly, and UnicodeError caught alongside OSError
213
+ # (opensrm-t4rd edge-case pass). Without the encoding, `write_text` uses the
214
+ # locale's, so on a non-UTF-8 system an ordinary accented team name raised
215
+ # UnicodeEncodeError -- which is a ValueError, NOT an OSError, so the handler
216
+ # below missed it and init died by traceback. Worse, `write_text` had already
217
+ # created the file, so a ZERO-BYTE manifest was left behind and every later
218
+ # run then hit the `exists()` guard above and refused, permanently, until
219
+ # someone deleted it by hand.
220
+ #
221
+ # The unlink is the rollback half of this bead's defect 3: a run that fails
222
+ # must not leave a partial artifact that blocks the next one.
199
223
  try:
200
- service_file.write_text(service_content)
201
- except OSError as e:
224
+ service_file.write_text(service_content, encoding="utf-8")
225
+ except (OSError, UnicodeError) as e:
202
226
  error(f"Error creating service file: {e}")
227
+ try:
228
+ service_file.unlink(missing_ok=True)
229
+ except OSError:
230
+ pass
203
231
  return 1
204
232
 
205
233
  # Create .nthlayer directory
@@ -209,20 +237,40 @@ def init_command(
209
237
  except OSError as e:
210
238
  warning(f"Could not create .nthlayer directory: {e}")
211
239
 
212
- # Create config file if it doesn't exist
240
+ # Create config file if it doesn't exist.
241
+ #
242
+ # `is_file()`, NOT `exists()` -- the same confusion this bead fixed for
243
+ # `nthlayer_dir` below. With `.nthlayer/config.yaml` present as a DIRECTORY,
244
+ # `exists()` is True because it is a directory, so the write was skipped and
245
+ # init reported success for a config it had never written, with no warning.
213
246
  config_file = nthlayer_dir / "config.yaml"
214
- if not config_file.exists():
247
+ if not config_file.is_file():
215
248
  config_content = _generate_config_yaml()
216
249
  try:
217
- config_file.write_text(config_content)
218
- except OSError as e:
250
+ config_file.write_text(config_content, encoding="utf-8")
251
+ except (OSError, UnicodeError) as e:
219
252
  warning(f"Could not create config file: {e}")
220
253
 
221
254
  # Success message
222
255
  console.print()
223
256
  success(f"Created {service_file}")
224
- if nthlayer_dir.exists():
257
+ # is_dir(), NOT exists() (opensrm-t4rd): with .nthlayer a regular FILE,
258
+ # exists() is True *because it is a file*, so init claimed to have created
259
+ # a directory it had not.
260
+ if nthlayer_dir.is_dir():
225
261
  success(f"Created {nthlayer_dir}/")
262
+ else:
263
+ # NOT fatal, including under --no-interactive. Decided deliberately
264
+ # (opensrm-t4rd acceptance): the manifest is the primary artifact and it
265
+ # was written correctly, so a CI gate keying on the exit code is right to
266
+ # pass. What was wrong before was claiming the directory had been created;
267
+ # the warning below states plainly that it was not, and names the fix.
268
+ # Making it fatal would fail runs whose manifest is perfectly good merely
269
+ # because .nthlayer could not be created (read-only parent, a stray file).
270
+ warning(
271
+ f"{nthlayer_dir}/ was not created — config was not written. "
272
+ f"It already exists as a file, or the parent is not writable."
273
+ )
226
274
 
227
275
  console.print()
228
276
  console.print("[bold]Next steps:[/bold]")
@@ -241,28 +289,147 @@ def init_command(
241
289
  return 0
242
290
 
243
291
 
244
- def _is_valid_service_name(name: str) -> bool:
245
- """Check if service name is valid (lowercase with hyphens).
292
+ # WHY THE SCALAR HELPERS BELOW EXIST (opensrm-t4rd).
293
+ #
294
+ # generate's hard rule 5 — no raw string construction for generated output —
295
+ # applied at the one input that is scriptable and unvalidated. `--team` was
296
+ # interpolated raw with only a truthiness check. Measured before the fix, all
297
+ # exiting 0:
298
+ #
299
+ # --team 'Platform: Core' -> the manifest failed to parse
300
+ # --team $'ops\nname: hijacked' -> injected a second service.name
301
+ # --team $'ops\ntier: critical' -> manifest VALIDATED CLEAN carrying a
302
+ # bogus tier shadowed by the real one
303
+ #
304
+ # A colon in a team name is ordinary, so quoting has to be available. It is
305
+ # applied conditionally, for the reason in `_yaml_scalar`, and done with
306
+ # pyyaml rather than `json.dumps`, for the reason in `_quoted_yaml_scalar`.
307
+ #
308
+ # The allowlist is deliberately narrow: anything outside it is quoted rather
309
+ # than reasoned about. It excludes every YAML indicator character (`:` `#`
310
+ # `-` at the start, `{` `[` `&` `*` `!` `|` `>` `%` `@` and a backtick),
311
+ # leading whitespace, and anything non-ASCII. Matching it is necessary but NOT
312
+ # sufficient — see `_emits_as_same_string`.
313
+ _PLAIN_SCALAR_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9 ._-]*")
246
314
 
247
- Args:
248
- name: Service name to validate
249
315
 
250
- Returns:
251
- True if valid
316
+ def _yaml_scalar(value: object) -> str:
317
+ """Render *value* as a YAML scalar that cannot alter the document.
318
+
319
+ Plain when it provably round-trips, quoted otherwise. Quoting is
320
+ conditional so the documented example output stays byte-identical to what
321
+ init writes (``test_block_is_byte_identical_to_real_output``, the
322
+ docs-vs-reality guard from opensrm-noc6).
323
+
324
+ A non-str is quoted rather than rejected: values arrive straight from a
325
+ YAML loader, so a template declaring ``name: on`` makes this a bool and
326
+ ``re.fullmatch`` would raise on it.
252
327
  """
253
- if not name:
328
+ if not isinstance(value, str):
329
+ return _quoted_yaml_scalar(str(value))
330
+ if _PLAIN_SCALAR_RE.fullmatch(value) and _emits_as_same_string(value):
331
+ return value
332
+ return _quoted_yaml_scalar(value)
333
+
334
+
335
+ # pyyaml wraps long scalars by default, which would emit a second line into a
336
+ # document built by string interpolation. Large enough to never wrap.
337
+ _NO_WRAP = 10**9
338
+
339
+
340
+ def _quoted_yaml_scalar(value: str) -> str:
341
+ """Render *value* as a double-quoted YAML scalar on exactly one line.
342
+
343
+ ``default_style='"'`` forces the quoted form for every input, so the result
344
+ is always a single line and never a block scalar.
345
+
346
+ ``json.dumps`` was used here first, on the reasoning that a JSON string is
347
+ also a valid YAML double-quoted scalar. True for the BMP, false above it:
348
+ with ``ensure_ascii=True`` json escapes a non-BMP character as a UTF-16
349
+ surrogate PAIR, and pyyaml resolves each 16-bit escape separately without
350
+ recombining them, so an emoji team name read back as two lone surrogates,
351
+ validated clean, and could not be re-encoded to UTF-8 at all.
352
+ ``ensure_ascii=False`` is no better: it mismatches on U+0085 and makes
353
+ pyyaml raise on U+007F.
354
+
355
+ ``allow_unicode=True`` is readability only, not correctness: it emits BMP
356
+ non-ASCII literally (``cafe`` with its accent, rather than an escape).
357
+ Above the BMP pyyaml escapes regardless of the flag -- but as ONE 32-bit
358
+ escape, which its own reader resolves back to one character. That single
359
+ escape, not the flag, is what json's surrogate pair got wrong. Measured:
360
+ both settings round-trip every value in the corpus.
361
+
362
+ The trailing newline pyyaml adds is stripped. No document-end marker is
363
+ stripped, because ``default_style='"'`` means only pyyaml's
364
+ double-quoted writer ever runs, and that writer never emits one.
365
+ """
366
+ return yaml.safe_dump(value, default_style='"', allow_unicode=True, width=_NO_WRAP).rstrip("\n")
367
+
368
+
369
+ def _emits_as_same_string(value: str) -> bool:
370
+ """True if *value* written as a plain YAML scalar reads back identically.
371
+
372
+ The allowlist excludes every YAML *indicator*, but says nothing about
373
+ implicit *type resolution*, which is the half that was missing. Measured,
374
+ every one of these matched the allowlist, was emitted unquoted, and loaded
375
+ back as something other than the string written -- all at exit 0:
376
+
377
+ --team null -> None -> validate FAILS, "team is required"
378
+ --team yes/on/true -> True -> validates CLEAN, bool in a str field
379
+ --team no/off/false -> False -> validate FAILS
380
+ --team 123 -> 123 -> int
381
+ --team 1_000 -> 1000 -> int
382
+ --team 1.5 -> 1.5 -> float
383
+ --team 0x1f -> 31 -> int
384
+ --team 2026-01-01 -> date(...) -> datetime.date, validates CLEAN
385
+ --team 'ops ' -> 'ops' -> trailing space silently stripped
386
+
387
+ Case variants resolve too (``NULL``, ``Yes``), which is why this asks the
388
+ loader instead of carrying a deny list that would have to track the YAML 1.1
389
+ type schema -- and stay correct as pyyaml changes. Both shapes above are the
390
+ bead's own defects one field over: the first writes a manifest generate's
391
+ validator rejects, the second a clean manifest carrying a wrong value.
392
+
393
+ Checking the bare scalar is faithful to the mapping-value position it is
394
+ emitted into, because the allowlist already excludes every character whose
395
+ resolution differs between those two contexts.
396
+
397
+ ``ValueError`` is caught alongside ``YAMLError`` because pyyaml's integer
398
+ constructor raises it rather than a YAML error on an allowlist-matching
399
+ input: ``0b_`` reaches ``int("", 2)``. Uncaught, that crashed ``nthlayer
400
+ init`` with a traceback and wrote nothing. Fuzzing every
401
+ allowlist-matching string up to length 4 (10248 of them) reaches exactly
402
+ these two types, ``ValueError`` on 3 inputs -- ``0b_``, ``0x_``, ``0b__``.
403
+
404
+ Either way the answer is the same: a value that will not load is not
405
+ provably a plain string, so it gets quoted.
406
+ """
407
+ try:
408
+ loaded = yaml.safe_load(value)
409
+ except (yaml.YAMLError, ValueError):
254
410
  return False
411
+ return isinstance(loaded, str) and loaded == value
255
412
 
256
- # Must be lowercase, numbers, and hyphens only
257
- # Must not start or end with hyphen
258
- if name[0] == "-" or name[-1] == "-":
259
- return False
260
413
 
261
- for char in name:
262
- if not (char.islower() or char.isdigit() or char == "-"):
263
- return False
414
+ def _is_valid_team(team: str) -> bool:
415
+ r"""True if *team* contains no line break, tab or NUL.
416
+
417
+ Those four only, not every control character: the rest are quoted correctly
418
+ by ``_yaml_scalar`` and are merely odd, not dangerous. Blankness is not
419
+ checked here either — the caller rejects it first, with a message that
420
+ actually fits it.
421
+
422
+ Quoting alone already makes the document safe, so this is not what prevents
423
+ injection. It is what turns a newline in --team into a clear error rather
424
+ than a silently escaped ``\n`` in the output, because a team name spanning
425
+ lines is a mistake every time.
426
+ """
427
+ return not any(ch in team for ch in "\n\r\t\x00")
428
+
264
429
 
265
- return True
430
+ def _is_valid_service_name(name: str) -> bool:
431
+ """Delegates to the one service-name rule; see specs/manifest for why."""
432
+ return is_valid_service_name(name)
266
433
 
267
434
 
268
435
  def _generate_service_yaml_v2(
@@ -293,14 +460,14 @@ def _generate_service_yaml_v2(
293
460
  resources_yaml = _build_resources_yaml(service_name, tier, service_type, dependencies)
294
461
 
295
462
  # Template line if using a template
296
- template_line = f" template: {template.name}\n" if template else ""
463
+ template_line = f" template: {_yaml_scalar(template.name)}\n" if template else ""
297
464
 
298
465
  return f"""# {service_name} Service Definition
299
466
  # Generated by NthLayer
300
467
 
301
468
  service:
302
- name: {service_name}
303
- team: {team}
469
+ name: {_yaml_scalar(service_name)}
470
+ team: {_yaml_scalar(team)}
304
471
  tier: {tier}
305
472
  type: {service_type}
306
473
  {template_line}
@@ -457,11 +624,11 @@ def _generate_service_yaml(service_name: str, team: str, template) -> str:
457
624
  # Generated by NthLayer
458
625
 
459
626
  service:
460
- name: {service_name}
461
- team: {team}
627
+ name: {_yaml_scalar(service_name)}
628
+ team: {_yaml_scalar(team)}
462
629
  tier: {template.tier} # critical | standard | low
463
630
  type: {template.type} # api | worker | stream | batch | database | ai-gate | x-web
464
- template: {template.name}
631
+ template: {_yaml_scalar(template.name)}
465
632
 
466
633
  # Template provides:
467
634
  {_format_template_resources(template)}
@@ -493,7 +660,12 @@ def _format_template_resources(template) -> str:
493
660
  """
494
661
  lines = []
495
662
  for resource in template.resources:
496
- lines.append(f"# - {resource.kind}: {resource.name}")
663
+ # A comment, but still a line in the document: `resource.kind` and
664
+ # `resource.name` come from a template file on disk with no
665
+ # validation, so a newline in either would escape the `#` and land
666
+ # at top level. `_yaml_scalar` escapes it instead, and leaves the
667
+ # ordinary values (`SLO`, `availability`) unquoted.
668
+ lines.append(f"# - {_yaml_scalar(resource.kind)}: {_yaml_scalar(resource.name)}")
497
669
  return "\n".join(lines) if lines else "# (no resources)"
498
670
 
499
671
 
@@ -17,6 +17,12 @@ import argparse
17
17
  import os
18
18
  from pathlib import Path
19
19
 
20
+ # Reused from init rather than copied, which is this bead's whole point
21
+ # (opensrm-h9fq). It is the only private cross-module import in src/, and
22
+ # relocating these helpers to a shared home is tracked in opensrm-d5ce --
23
+ # deferred because opensrm-t4rd's AST guards match them by name in cli/init.py
24
+ # and that move needs its own verification.
25
+ from nthlayer_generate.cli.init import _is_valid_team, _yaml_scalar
20
26
  from nthlayer_generate.cli.ux import (
21
27
  confirm,
22
28
  console,
@@ -42,6 +48,7 @@ from nthlayer_generate.config.loader import (
42
48
  save_config,
43
49
  )
44
50
  from nthlayer_generate.config.secrets import get_secret_resolver
51
+ from nthlayer_generate.specs.manifest import is_valid_service_name
45
52
 
46
53
 
47
54
  def setup_command(
@@ -88,11 +95,23 @@ def setup_command(
88
95
  print()
89
96
  _test_connections()
90
97
 
91
- # Offer to create first service
98
+ # Offer to create first service.
99
+ #
100
+ # The return value is PROPAGATED (opensrm-h9fq). It used to be discarded,
101
+ # so every failure inside printed `error(...)` and then this function
102
+ # printed "Setup Complete!" and returned 0. The configuration above IS
103
+ # saved by this point, so the message says what actually happened rather
104
+ # than claiming either total success or total failure.
105
+ service_rc = 0
92
106
  if not skip_service:
93
107
  print()
94
108
  if _confirm("Create your first service?", default=True):
95
- _create_first_service()
109
+ service_rc = _create_first_service()
110
+
111
+ if service_rc != 0:
112
+ console.print()
113
+ warning("Configuration was saved, but the first service was not created.")
114
+ return service_rc
96
115
 
97
116
  _print_next_steps()
98
117
  return 0
@@ -375,8 +394,36 @@ def _test_pagerduty(config: IntegrationConfig) -> tuple[bool, str]:
375
394
  return False, str(e)
376
395
 
377
396
 
378
- def _create_first_service() -> None:
379
- """Guide user through creating their first service."""
397
+ # The wizard's three tier tables, lifted to module level so a test can assert
398
+ # they AGREE (opensrm-h9fq). While they were function-locals, adding a value to
399
+ # `tier_map` with no matching config was an undetectable drift -- the provenance
400
+ # pass mutated exactly that and the suite stayed green.
401
+ TIER_CHOICES = [
402
+ "critical - 99.95% availability, 5min escalation",
403
+ "standard - 99.9% availability, 15min escalation",
404
+ "low - 99.5% availability, 30min escalation",
405
+ ]
406
+ TIER_MAP = {"critical": 1, "standard": 2, "low": 3}
407
+ TIER_CONFIGS_BY_INDEX = {
408
+ 1: {"availability": 99.95, "latency_ms": 200, "tier_name": "critical"},
409
+ 2: {"availability": 99.9, "latency_ms": 500, "tier_name": "standard"},
410
+ 3: {"availability": 99.5, "latency_ms": 1000, "tier_name": "low"},
411
+ }
412
+
413
+
414
+ def _create_first_service() -> int:
415
+ """Guide user through creating their first service.
416
+
417
+ Returns 0 on success OR on a deliberate skip, and 1 on failure, so the
418
+ caller can key an exit code on it (opensrm-h9fq). It returned None while
419
+ every failure path printed `error(...)` and the command then printed
420
+ "Setup Complete!" and exited 0 -- which is this bead's own defect in its
421
+ exit-code half, and `specs/manifest.py` says plainly that an exit code is
422
+ what a CI gate keys on.
423
+
424
+ A user declining is NOT a failure: choosing not to create a service, or
425
+ declining an overwrite, returns 0.
426
+ """
380
427
  console.print()
381
428
  console.print("[bold]Create Your First Service[/bold]")
382
429
  console.print("[muted]─[/muted]" * 40)
@@ -385,16 +432,29 @@ def _create_first_service() -> None:
385
432
  service_name = _prompt("Service name (e.g., payment-api)")
386
433
  if not service_name:
387
434
  info("Skipping service creation.")
388
- return
435
+ return 0
389
436
 
390
437
  # Validate name
391
438
  if not _is_valid_service_name(service_name):
392
439
  error(f"Invalid service name: {service_name}")
393
- console.print("Use lowercase letters, numbers, and hyphens only.")
394
- return
440
+ console.print(" [muted]Use lowercase letters, numbers and hyphens only[/muted]")
441
+ return 1
395
442
 
396
443
  team = _prompt("Team name", default="platform")
397
444
 
445
+ # Gated and quoted for the same reason as init (opensrm-t4rd, opensrm-h9fq):
446
+ # `team` is free text and was interpolated raw, so a colon broke the document
447
+ # and a newline injected a sibling field that validated clean. `_yaml_scalar`
448
+ # makes the document safe; this check turns a newline into a clear error
449
+ # rather than a silently escaped one.
450
+ if not team or not team.strip():
451
+ error("Team name is required")
452
+ return 1
453
+ if not _is_valid_team(team):
454
+ error("Invalid team name")
455
+ console.print(" [muted]Team name must not contain line breaks, tabs or NUL[/muted]")
456
+ return 1
457
+
398
458
  # Service type selection using interactive menu
399
459
  type_choices = [
400
460
  "api - HTTP/REST API service",
@@ -405,32 +465,98 @@ def _create_first_service() -> None:
405
465
  service_type = selected_type.split(" - ")[0]
406
466
 
407
467
  # Service tier selection using interactive menu
408
- tier_choices = [
409
- "critical - 99.95% availability, 5min escalation",
410
- "standard - 99.9% availability, 15min escalation",
411
- "low - 99.5% availability, 30min escalation",
412
- ]
468
+ tier_choices = list(TIER_CHOICES)
413
469
  selected_tier = select("Service tier", tier_choices, default=tier_choices[1])
414
470
  tier_name = selected_tier.split(" - ")[0]
415
- tier_map = {"critical": 1, "standard": 2, "low": 3}
471
+ tier_map = dict(TIER_MAP)
416
472
  tier = tier_map.get(tier_name, 2)
417
473
 
418
- # Create services directory
474
+ # All three of the following mirror cli/init.py's hardening from
475
+ # opensrm-t4rd (opensrm-h9fq). This bead imported init's quoting HELPERS and
476
+ # left the file IO beside them unhardened, so t4rd's defects were still live
477
+ # here verbatim.
478
+ #
479
+ # `exist_ok=True` does not cover a non-directory last component, so a
480
+ # `services` FILE or dangling symlink raised FileExistsError, and a
481
+ # read-only cwd raised PermissionError -- a traceback where init gives a
482
+ # message. Not fatal, as in init: the wizard has nothing else to write.
419
483
  services_dir = Path("services")
420
- services_dir.mkdir(exist_ok=True)
484
+ try:
485
+ services_dir.mkdir(exist_ok=True)
486
+ except OSError as e:
487
+ error(f"Could not create {services_dir}/: {e}")
488
+ console.print(
489
+ " [muted]It may already exist as a file, or the parent may not be "
490
+ "writable. Remove or rename it and re-run.[/muted]"
491
+ )
492
+ return 1
421
493
 
422
494
  # Generate service YAML
423
495
  service_content = _generate_service_yaml(service_name, team, service_type, tier)
424
496
 
497
+ # `is_file()`, NOT `exists()`. With a DIRECTORY at the target, `exists()` was
498
+ # True, so the user was asked to confirm an overwrite and then got
499
+ # IsADirectoryError. The same confusion t4rd corrected in init.
425
500
  service_file = services_dir / f"{service_name}.yaml"
426
- if service_file.exists():
501
+ if service_file.is_file():
427
502
  if not _confirm(f"{service_file} exists. Overwrite?", default=False):
428
503
  info("Skipping service creation.")
429
- return
430
-
431
- service_file.write_text(service_content)
504
+ return 0
505
+ elif service_file.exists():
506
+ error(f"{service_file} exists but is not a regular file")
507
+ console.print(" [muted]Remove it, or choose a different service name.[/muted]")
508
+ return 1
509
+
510
+ # ENCODE BEFORE OPENING ANYTHING. `write_text` opens with "w", which
511
+ # truncates immediately, so an encoding failure lands AFTER the file is
512
+ # already empty. Measured: a 38-byte manifest went to 0 bytes and the user
513
+ # was told it had been left unchanged. Encoding first moves the whole
514
+ # UnicodeError class in front of any file descriptor, so on that path
515
+ # nothing is touched and saying so is true.
516
+ #
517
+ # DEFENSIVE ONLY: no known wizard input reaches this handler, and the
518
+ # provenance pass confirmed it cannot be kill-checked. Kept because a field
519
+ # added later without `_yaml_scalar` would make it reachable (opensrm-h9fq).
520
+ try:
521
+ payload = service_content.encode("utf-8")
522
+ except UnicodeError as e:
523
+ error(f"Could not encode the manifest: {e}")
524
+ info(f"{service_file} was not touched.")
525
+ return 1
526
+
527
+ # `is_symlink()` as well as `is_file()`. A symlink whose victim is missing
528
+ # is neither a file nor `exists()`, so without this the rollback below
529
+ # deleted a symlink this run did not create -- the same mistake as deleting
530
+ # a pre-existing regular file, one shape over.
531
+ #
532
+ pre_existing = service_file.is_file() or service_file.is_symlink()
533
+ try:
534
+ service_file.write_bytes(payload)
535
+ except OSError as e:
536
+ error(f"Error creating {service_file}: {e}")
537
+ if pre_existing:
538
+ # Deliberately not "unchanged": an OSError at open leaves it intact,
539
+ # but one raised mid-write leaves it partial, and this cannot tell
540
+ # them apart. Atomic replacement is opensrm-may6's scope; until then
541
+ # the message must not promise more than it knows.
542
+ info(
543
+ f"{service_file} was not removed, but may be incomplete. "
544
+ f"Check it with `nthlayer validate {service_file}`, or re-run "
545
+ f"`nthlayer setup`."
546
+ )
547
+ else:
548
+ try:
549
+ service_file.unlink(missing_ok=True)
550
+ except OSError:
551
+ # intentionally ignored: the write already failed and the
552
+ # manifest is the only artifact, so a failed cleanup has
553
+ # nothing left to report and must not mask the real error
554
+ # (hard rule 4).
555
+ pass
556
+ return 1
432
557
  console.print()
433
558
  success(f"Created {service_file}")
559
+ return 0
434
560
 
435
561
 
436
562
  def _generate_service_yaml(
@@ -441,20 +567,24 @@ def _generate_service_yaml(
441
567
  ) -> str:
442
568
  """Generate service YAML content."""
443
569
  # Tier-based defaults
444
- tier_configs = {
445
- 1: {"availability": 99.95, "latency_ms": 200, "tier_name": "critical"},
446
- 2: {"availability": 99.9, "latency_ms": 500, "tier_name": "standard"},
447
- 3: {"availability": 99.5, "latency_ms": 1000, "tier_name": "low"},
448
- }
449
- config = tier_configs.get(tier, tier_configs[2])
570
+ tier_configs = TIER_CONFIGS_BY_INDEX
571
+ # Raise rather than defaulting (opensrm-h9fq). The hazard is a STRING tier:
572
+ # init's sibling `_generate_service_yaml_v2` takes `tier: str`, so now that
573
+ # the emitted field is a name, a caller passing the name would otherwise get
574
+ # a wrong-but-valid manifest. Unreachable today, since `tier_map` clamps the
575
+ # menu to 1-3, so this is a loud guard on a latent silent-wrong rather than
576
+ # a live fix. The cases are enumerated in TestUnknownTierIsLoud.
577
+ if tier not in tier_configs:
578
+ raise ValueError(f"Unknown tier {tier!r}; expected one of {sorted(tier_configs)}")
579
+ config = tier_configs[tier]
450
580
 
451
581
  return f"""# {name} Service Definition
452
582
  # Generated by NthLayer setup wizard
453
583
 
454
584
  service:
455
- name: {name}
456
- team: {team}
457
- tier: {tier}
585
+ name: {_yaml_scalar(name)}
586
+ team: {_yaml_scalar(team)}
587
+ tier: {config["tier_name"]}
458
588
  type: {service_type}
459
589
 
460
590
  resources:
@@ -462,7 +592,7 @@ resources:
462
592
  - kind: SLO
463
593
  name: availability
464
594
  spec:
465
- objective: {config['availability']}
595
+ objective: {config["availability"]}
466
596
  window: 30d
467
597
  indicator:
468
598
  type: availability
@@ -476,7 +606,7 @@ resources:
476
606
  spec:
477
607
  objective: 99.0
478
608
  window: 30d
479
- threshold_ms: {config['latency_ms']}
609
+ threshold_ms: {config["latency_ms"]}
480
610
  indicator:
481
611
  type: latency
482
612
  percentile: 99
@@ -503,15 +633,11 @@ resources:
503
633
 
504
634
 
505
635
  def _is_valid_service_name(name: str) -> bool:
506
- """Validate service name format."""
507
- if not name:
508
- return False
509
- if name[0] == "-" or name[-1] == "-":
510
- return False
511
- for char in name:
512
- if not (char.islower() or char.isdigit() or char == "-"):
513
- return False
514
- return True
636
+ """Delegates to the one service-name rule; see specs/manifest for why.
637
+
638
+ This was the third private copy (opensrm-h9fq).
639
+ """
640
+ return is_valid_service_name(name)
515
641
 
516
642
 
517
643
  def _print_next_steps() -> None: