code-node-graph 0.5.0__tar.gz → 0.6.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 (189) hide show
  1. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/CHANGELOG.md +15 -1
  2. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/PKG-INFO +1 -1
  3. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/__init__.py +1 -1
  4. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/analysis/model.py +12 -0
  5. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/history.py +6 -1
  6. code_node_graph-0.6.0/src/code_node/report/changes.py +116 -0
  7. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/graph.py +91 -37
  8. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/style.py +53 -14
  9. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/tests/test_code_node.py +5 -1
  10. code_node_graph-0.5.0/src/code_node/report/changes.py +0 -42
  11. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/.gitignore +0 -0
  12. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/LICENSE +0 -0
  13. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/README.md +0 -0
  14. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/examples/books-api/README.md +0 -0
  15. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/examples/cart-service/README.md +0 -0
  16. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/examples/orders-service/README.md +0 -0
  17. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/pyproject.toml +0 -0
  18. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/__main__.py +0 -0
  19. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/analysis/__init__.py +0 -0
  20. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/analysis/aws_model.py +0 -0
  21. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/analysis/findings.py +0 -0
  22. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/analysis/graph.py +0 -0
  23. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/analysis/mapping.py +0 -0
  24. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/analysis/reader.py +0 -0
  25. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/analysis/syntax.py +0 -0
  26. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/analysis/terms.py +0 -0
  27. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/cli.py +0 -0
  28. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/deploys/__init__.py +0 -0
  29. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/deploys/sam.py +0 -0
  30. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/deploys/serverless.py +0 -0
  31. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/kit/__init__.py +0 -0
  32. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/kit/blocks.py +0 -0
  33. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/kit/fmt.py +0 -0
  34. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/kit/highlight.py +0 -0
  35. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/kit/render.py +0 -0
  36. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/kit/text.py +0 -0
  37. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/libraries/__init__.py +0 -0
  38. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/libraries/llm.py +0 -0
  39. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/libraries/mongo.py +0 -0
  40. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/libraries/redis.py +0 -0
  41. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/libraries/sql.py +0 -0
  42. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/pages.py +0 -0
  43. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/platforms/__init__.py +0 -0
  44. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/platforms/aws_lambda.py +0 -0
  45. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/platforms/web.py +0 -0
  46. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/project.py +0 -0
  47. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/__init__.py +0 -0
  48. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/brands.py +0 -0
  49. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/build.py +0 -0
  50. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/editor.py +0 -0
  51. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/README.md +0 -0
  52. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/anthropic-icon.svg +0 -0
  53. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-amplify.svg +0 -0
  54. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-api-gateway.svg +0 -0
  55. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-appflow.svg +0 -0
  56. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-appsync.svg +0 -0
  57. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-athena.svg +0 -0
  58. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-backup.svg +0 -0
  59. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-batch.svg +0 -0
  60. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-certificate-manager.svg +0 -0
  61. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-cloudformation.svg +0 -0
  62. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-cloudfront.svg +0 -0
  63. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-cloudtrail.svg +0 -0
  64. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-cloudwatch.svg +0 -0
  65. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-codebuild.svg +0 -0
  66. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-codecommit.svg +0 -0
  67. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-codedeploy.svg +0 -0
  68. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-codepipeline.svg +0 -0
  69. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-cognito.svg +0 -0
  70. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-config.svg +0 -0
  71. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-documentdb.svg +0 -0
  72. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-dynamodb.svg +0 -0
  73. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-ec2.svg +0 -0
  74. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-ecs.svg +0 -0
  75. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-eks.svg +0 -0
  76. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-elasticache.svg +0 -0
  77. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-elb.svg +0 -0
  78. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-eventbridge.svg +0 -0
  79. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-glacier.svg +0 -0
  80. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-glue.svg +0 -0
  81. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-iam.svg +0 -0
  82. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-keyspaces.svg +0 -0
  83. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-kinesis.svg +0 -0
  84. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-kms.svg +0 -0
  85. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-lake-formation.svg +0 -0
  86. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-lambda.svg +0 -0
  87. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-lightsail.svg +0 -0
  88. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-mq.svg +0 -0
  89. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-msk.svg +0 -0
  90. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-neptune.svg +0 -0
  91. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-open-search.svg +0 -0
  92. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-quicksight.svg +0 -0
  93. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-rds.svg +0 -0
  94. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-redshift.svg +0 -0
  95. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-route53.svg +0 -0
  96. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-s3.svg +0 -0
  97. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-secrets-manager.svg +0 -0
  98. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-ses.svg +0 -0
  99. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-shield.svg +0 -0
  100. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-sns.svg +0 -0
  101. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-sqs.svg +0 -0
  102. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-step-functions.svg +0 -0
  103. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-systems-manager.svg +0 -0
  104. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-timestream.svg +0 -0
  105. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-waf.svg +0 -0
  106. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws-xray.svg +0 -0
  107. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/aws.svg +0 -0
  108. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/azure-icon.svg +0 -0
  109. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/django-icon.svg +0 -0
  110. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/elasticsearch.svg +0 -0
  111. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/fastapi-icon.svg +0 -0
  112. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/firebase-icon.svg +0 -0
  113. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/flask.svg +0 -0
  114. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/google-cloud.svg +0 -0
  115. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/kafka-icon.svg +0 -0
  116. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/mariadb-icon.svg +0 -0
  117. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/mongodb-icon.svg +0 -0
  118. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/mysql-icon.svg +0 -0
  119. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/openai-icon.svg +0 -0
  120. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/postgresql.svg +0 -0
  121. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/python.svg +0 -0
  122. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/rabbitmq-icon.svg +0 -0
  123. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/redis.svg +0 -0
  124. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/serverless.svg +0 -0
  125. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/slack-icon.svg +0 -0
  126. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-api-gateway.svg +0 -0
  127. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-appflow.svg +0 -0
  128. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-appsync.svg +0 -0
  129. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-athena.svg +0 -0
  130. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-backup.svg +0 -0
  131. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-batch.svg +0 -0
  132. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-certificate-manager.svg +0 -0
  133. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-cloudformation.svg +0 -0
  134. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-cloudfront.svg +0 -0
  135. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-cloudtrail.svg +0 -0
  136. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-cloudwatch.svg +0 -0
  137. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-codebuild.svg +0 -0
  138. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-codecommit.svg +0 -0
  139. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-codedeploy.svg +0 -0
  140. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-codepipeline.svg +0 -0
  141. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-cognito.svg +0 -0
  142. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-config.svg +0 -0
  143. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-documentdb.svg +0 -0
  144. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-dynamodb.svg +0 -0
  145. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-ec2.svg +0 -0
  146. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-ecs.svg +0 -0
  147. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-eks.svg +0 -0
  148. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-elasticache.svg +0 -0
  149. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-elb.svg +0 -0
  150. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-eventbridge.svg +0 -0
  151. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-glacier.svg +0 -0
  152. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-glue.svg +0 -0
  153. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-iam.svg +0 -0
  154. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-keyspaces.svg +0 -0
  155. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-kinesis.svg +0 -0
  156. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-kms.svg +0 -0
  157. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-lake-formation.svg +0 -0
  158. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-lambda.svg +0 -0
  159. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-mq.svg +0 -0
  160. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-msk.svg +0 -0
  161. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-neptune.svg +0 -0
  162. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-open-search.svg +0 -0
  163. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-quicksight.svg +0 -0
  164. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-rds.svg +0 -0
  165. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-redshift.svg +0 -0
  166. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-route53.svg +0 -0
  167. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-s3.svg +0 -0
  168. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-secrets-manager.svg +0 -0
  169. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-ses.svg +0 -0
  170. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-shield.svg +0 -0
  171. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-sns.svg +0 -0
  172. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-sqs.svg +0 -0
  173. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-step-functions.svg +0 -0
  174. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-systems-manager.svg +0 -0
  175. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-timestream.svg +0 -0
  176. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-waf.svg +0 -0
  177. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/small/aws-xray.svg +0 -0
  178. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/snowflake-icon.svg +0 -0
  179. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/sqlite.svg +0 -0
  180. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/stripe.svg +0 -0
  181. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/supabase-icon.svg +0 -0
  182. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/logos/twilio-icon.svg +0 -0
  183. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/outline.py +0 -0
  184. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/tabs.py +0 -0
  185. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/report/walk.py +0 -0
  186. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/src/code_node/serve.py +0 -0
  187. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/tests/snapshots/books-api.txt +0 -0
  188. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/tests/snapshots/cart-service.txt +0 -0
  189. {code_node_graph-0.5.0 → code_node_graph-0.6.0}/tests/snapshots/orders-service.txt +0 -0
@@ -4,6 +4,19 @@ Every change a user would notice, newest first ([Keep a Changelog](https://keepa
4
4
  follow [Semantic Versioning](https://semver.org/); before 1.0, a minor version may change what a command does.
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.0] - 2026-10-20
8
+
9
+ ### Changed
10
+
11
+ - A function's code panel says beside each line who changed it last and how long ago ("Asha · 3d"), with a bar
12
+ that's darker the newer the line and amber for a line not committed yet. Its last row says when the function was
13
+ last changed, by whom, in which commit, and how many commits and people it comes from; point at a line and it says
14
+ that line's commit instead. The gear by × opens the panel's settings, where the history can be turned off; the
15
+ page remembers that. The panel's head is plainer: the name, then what it is, where, and a link to open it in your
16
+ editor, without the def repeated over the code.
17
+ - Code on the page is set in Cascadia Code when it's installed, before Cascadia Mono and the system's monospace
18
+ font, and the docs site's code is in Cascadia Code instead of IBM Plex Mono.
19
+
7
20
  ## [0.5.0] - 2026-10-20
8
21
 
9
22
  ### Added
@@ -212,7 +225,8 @@ follow [Semantic Versioning](https://semver.org/); before 1.0, a minor version m
212
225
  - `--platform NAME` runs a platform's checks whatever the entry point is; `--lambda` is short for
213
226
  `--platform lambda`. `EntryPoint.is_lambda` and `PageOptions.is_lambda` are now `platform` (`"lambda"` or `""`).
214
227
 
215
- [Unreleased]: https://github.com/utkarsh5026/code-node/compare/v0.5.0...HEAD
228
+ [Unreleased]: https://github.com/utkarsh5026/code-node/compare/v0.6.0...HEAD
229
+ [0.6.0]: https://github.com/utkarsh5026/code-node/compare/v0.5.0...v0.6.0
216
230
  [0.5.0]: https://github.com/utkarsh5026/code-node/compare/v0.4.0...v0.5.0
217
231
  [0.4.0]: https://github.com/utkarsh5026/code-node/compare/v0.3.0...v0.4.0
218
232
  [0.3.0]: https://github.com/utkarsh5026/code-node/compare/v0.2.0...v0.3.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code-node-graph
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: See how Python code works without running it: a graph of who calls whom from an entry point, and what each path reaches.
5
5
  Project-URL: Homepage, https://github.com/utkarsh5026/code-node
6
6
  Project-URL: Documentation, https://utkarsh5026.github.io/code-node/
@@ -13,7 +13,7 @@ from .libraries import Library, register_library
13
13
  from .platforms import Platform, get_platform, register_platform
14
14
  from .project import EntryPoint, Project, entry_points, read_project
15
15
 
16
- __version__ = "0.5.0"
16
+ __version__ = "0.6.0"
17
17
 
18
18
  __all__ = [
19
19
  "CodeGraph",
@@ -204,6 +204,8 @@ class History:
204
204
  first: int # when the oldest of them was made (about when the function was written), in seconds since 1970
205
205
  uncommitted: int # its lines changed since the last commit
206
206
  now: int # when this was read, for 'three days ago'
207
+ lines: dict[int, Commit | None] = field(default_factory=dict) # each of its lines -> the commit it comes from,
208
+ # None for one changed since the last commit
207
209
 
208
210
  def ago(self, time: int) -> str:
209
211
  """
@@ -216,6 +218,16 @@ class History:
216
218
  return f"{count} {unit}{'' if count == 1 else 's'} ago"
217
219
  return "today"
218
220
 
221
+ def since(self, time: int) -> str:
222
+ """
223
+ The same, short, for beside a line: 'today', '3d', '2w', '5mo', '1y'.
224
+ """
225
+ days = max(0, self.now - time) // 86_400
226
+ for size, unit in ((365, "y"), (30, "mo"), (7, "w"), (1, "d")):
227
+ if days >= size:
228
+ return f"{days // size}{unit}"
229
+ return "today"
230
+
219
231
 
220
232
  @dataclass
221
233
  class CodeFunction:
@@ -1,6 +1,7 @@
1
1
  """
2
2
  What git says about the functions a map shows: `git blame` on the lines of each one, once per file, for the commit
3
- that last changed it, how many commits and people its lines come from, and the lines changed since the last commit.
3
+ that last changed it, how many commits and people its lines come from, the lines changed since the last commit, and
4
+ the commit each line comes from.
4
5
 
5
6
  git is run on the folder, never the code in it, and only to read: blame with no text conversion and no file system
6
7
  monitor. A folder whose own git config names a filter command (one git would run on the files) isn't read at all.
@@ -98,12 +99,14 @@ def _history(blamed: dict[int, tuple[str, dict[str, str]]], start: int, end: int
98
99
  commits: dict[str, Commit] = {}
99
100
  by_author: Counter[str] = Counter()
100
101
  uncommitted = 0
102
+ lines: dict[int, Commit | None] = {}
101
103
  for k in range(start, end + 1):
102
104
  if k not in blamed:
103
105
  continue
104
106
  sha, said = blamed[k]
105
107
  if sha == _UNCOMMITTED:
106
108
  uncommitted += 1
109
+ lines[k] = None
107
110
  continue
108
111
  if sha not in commits:
109
112
  commits[sha] = Commit(
@@ -114,6 +117,7 @@ def _history(blamed: dict[int, tuple[str, dict[str, str]]], start: int, end: int
114
117
  page.format(sha) if page else "",
115
118
  )
116
119
  by_author[commits[sha].author] += 1
120
+ lines[k] = commits[sha]
117
121
  last = max(commits.values(), key=lambda c: c.time, default=None)
118
122
  return History(
119
123
  last=last,
@@ -122,6 +126,7 @@ def _history(blamed: dict[int, tuple[str, dict[str, str]]], start: int, end: int
122
126
  first=min((c.time for c in commits.values()), default=0),
123
127
  uncommitted=uncommitted,
124
128
  now=now,
129
+ lines=lines,
125
130
  )
126
131
 
127
132
 
@@ -0,0 +1,116 @@
1
+ """
2
+ What git says about a function, as HTML: who changed it last and when, with a link to the commit when the repository
3
+ is on GitHub or GitLab, how many commits and people its lines come from, and its lines not committed yet. The same
4
+ in the graph's hover card and the walk's card, and in the graph's panel line by line, beside its code.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime, timezone
10
+
11
+ from ..kit.text import _esc
12
+
13
+ from ..analysis.model import Commit, History
14
+
15
+
16
+ def history_html(history: History | None, short: bool = False) -> str:
17
+ """
18
+ The function's history as a few lines, or '' without one. short: the last change alone, for a hover card.
19
+ """
20
+ if history is None or not (history.last or history.uncommitted):
21
+ return ""
22
+ said = []
23
+ last = history.last
24
+ if last:
25
+ day = datetime.fromtimestamp(last.time, timezone.utc).strftime("%Y-%m-%d")
26
+ sha = f"<code>{_esc(last.short)}</code>"
27
+ if last.url and not short:
28
+ sha = f'<a href="{_esc(last.url)}" target="_blank" rel="noopener" title="The commit">{sha} ↗</a>'
29
+ said.append(
30
+ f'Last changed <b title="{day}">{_esc(history.ago(last.time))}</b> by {_esc(last.author)} in {sha}'
31
+ + (f": <q>{_esc(last.subject)}</q>" if last.subject else "")
32
+ )
33
+ if history.commits > 1 and not short:
34
+ people = f"{len(history.authors)} people" if len(history.authors) > 1 else _esc(history.authors[0])
35
+ said.append(
36
+ f"Its lines come from {history.commits} commits by {people}, the oldest "
37
+ f"{_esc(history.ago(history.first))}"
38
+ )
39
+ if history.uncommitted:
40
+ changed = f"{history.uncommitted} line{'' if history.uncommitted == 1 else 's'} changed since the last commit"
41
+ said.append(f'<span class="cnhu">{changed if last else "Not committed yet"}</span>')
42
+ return '<div class="cnh">' + "".join(f"<p>{line}</p>" for line in said) + "</div>"
43
+
44
+
45
+ _AGES = 4 # shades of a line's bar, oldest to newest
46
+
47
+
48
+ def blame_keys(history: History) -> dict[str, int]:
49
+ """
50
+ The commits its lines come from, oldest first, each with its place in that order: what a line's mark names to
51
+ find its commit in the panel's last row.
52
+ """
53
+ commits = {c.sha: c for c in history.lines.values() if c}
54
+ return {sha: k for k, sha in enumerate(sorted(commits, key=lambda sha: (commits[sha].time, sha)))}
55
+
56
+
57
+ def blame_mark(history: History, line: int, keys: dict[str, int], same: bool) -> str:
58
+ """
59
+ What goes beside a line of the panel's code: who changed it last and how long ago (left out when the line above
60
+ says the same, same), over a bar darker the newer the line, amber for one not committed yet. Pointing at it says
61
+ the commit; '' for a line blame didn't cover, so the code stays in line.
62
+ """
63
+ if line not in history.lines:
64
+ return '<span class="sb"></span>'
65
+ commit = history.lines[line]
66
+ if commit is None:
67
+ return (
68
+ f'<span class="sb au{" same" if same else ""}" data-k="u" title="Changed since the last commit">'
69
+ "<span>Uncommitted</span></span>"
70
+ )
71
+ k = keys[commit.sha]
72
+ shade = _AGES - 1 if len(keys) == 1 else round(k * (_AGES - 1) / (len(keys) - 1))
73
+ said = f"{commit.subject} · {commit.author}, {history.ago(commit.time)} · {commit.short}"
74
+ return (
75
+ f'<span class="sb a{shade}{" same" if same else ""}" data-k="{k}" title="{_esc(said)}">'
76
+ f"<span>{_esc(commit.author.split(' ')[0] or commit.author)} · {_esc(history.since(commit.time))}</span></span>"
77
+ )
78
+
79
+
80
+ def blame_row(history: History, keys: dict[str, int]) -> str:
81
+ """
82
+ The panel's last row: when its code was last changed and by whom, then hidden, a row for each commit its lines come
83
+ from, which a script shows in its place while a line from that commit is pointed at.
84
+ """
85
+
86
+ def sha(commit: Commit) -> str:
87
+ short = f"<code>{_esc(commit.short)}</code>"
88
+ if not commit.url:
89
+ return f'<span class="csfh">{short}</span>'
90
+ return (
91
+ f'<a class="csfh" href="{_esc(commit.url)}" target="_blank" rel="noopener" title="The commit">{short}</a>'
92
+ )
93
+
94
+ last = history.last
95
+ if last:
96
+ day = datetime.fromtimestamp(last.time, timezone.utc).strftime("%Y-%m-%d")
97
+ said = f'Changed <b title="{day}">{_esc(history.ago(last.time))}</b> by {_esc(last.author)}'
98
+ if history.commits > 1:
99
+ people = len(history.authors)
100
+ said += f" · {history.commits} commits" + (f" by {people} people" if people > 1 else "")
101
+ else:
102
+ said = "Not committed yet"
103
+ if history.uncommitted and last:
104
+ lines = f"{history.uncommitted} line{'' if history.uncommitted == 1 else 's'}"
105
+ said += f' · <span class="cnhu">{lines} not committed</span>'
106
+ rows = [f'<p class="csfs"><span>{said}</span>{sha(last) if last else ""}</p>']
107
+ commits = {c.sha: c for c in history.lines.values() if c}
108
+ for found, k in keys.items():
109
+ commit = commits[found]
110
+ rows.append(
111
+ f'<p data-k="{k}" hidden><span><b>{_esc(commit.subject or commit.short)}</b> · {_esc(commit.author)}, '
112
+ f"{_esc(history.ago(commit.time))}</span>{sha(commit)}</p>"
113
+ )
114
+ if history.uncommitted:
115
+ rows.append('<p data-k="u" hidden><span class="cnhu">Changed since the last commit</span></p>')
116
+ return f'<div class="csf">{"".join(rows)}</div>'
@@ -19,7 +19,7 @@ from ..kit.text import _esc
19
19
 
20
20
  from ..analysis.findings import _short_name
21
21
  from ..analysis.graph import CodeGraph, GraphEdge, GraphNode, GraphView
22
- from .changes import history_html
22
+ from .changes import blame_keys, blame_mark, blame_row, history_html
23
23
  from .brands import AWS_LOGOS, SERVICE_LOGOS, fills, logo, small_logo
24
24
  from .editor import editor_name, open_at
25
25
  from .style import _ROOT
@@ -1131,6 +1131,18 @@ _G_PANEL_KINDS = {
1131
1131
  }
1132
1132
 
1133
1133
 
1134
+ _G_GEAR = ( # the panel's settings
1135
+ '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" '
1136
+ 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/>'
1137
+ '<path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21'
1138
+ "a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 "
1139
+ "1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1"
1140
+ "a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1"
1141
+ 'a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/>'
1142
+ "</svg>"
1143
+ )
1144
+
1145
+
1134
1146
  def _g_panel_id(n: GraphNode) -> str:
1135
1147
  return f"cn-code-{n.id}"
1136
1148
 
@@ -1146,9 +1158,12 @@ def _g_panel(n: GraphNode, lines_of: Callable[[str], list[str]], editor: str = "
1146
1158
  """
1147
1159
  A box's code, in a panel that slides in from the right when the box is clicked (its id, panel or the box's own, is
1148
1160
  the link's target, so it's CSS's :target that opens it): what the box is, then each place it's about with its
1149
- lines numbered, the lines worth pointing at lit. '' when none of its code is in the files read.
1161
+ lines numbered, the lines worth pointing at lit. A function's lines each say who changed them last and when, and
1162
+ its last row what git says about it, when the folder is in git. '' when none of its code is in the files read.
1150
1163
  """
1151
- shown, lit_any = [], False
1164
+ history = n.history if n.history and n.history.lines else None
1165
+ keys = blame_keys(history) if history else {}
1166
+ places = [] # (file, first line, last line, lines left out, the editor's link, its rows)
1152
1167
  for span in n.code[:_G_PANEL_SPANS]:
1153
1168
  lines = lines_of(span.file)
1154
1169
  start, end = max(1, span.start), min(len(lines), span.end)
@@ -1163,48 +1178,66 @@ def _g_panel(n: GraphNode, lines_of: Callable[[str], list[str]], editor: str = "
1163
1178
  continue
1164
1179
  more = end - start + 1 - _G_PANEL_LINES
1165
1180
  end = min(end, start + _G_PANEL_LINES - 1)
1166
- lit_any |= any(start <= k <= end for k in span.lit)
1167
- rows = "".join(
1168
- f'<span class="sl{" lit" if k in span.lit else ""}"><span class="sn">{k}</span>{lines[k - 1]}</span>'
1169
- for k in range(start, end + 1)
1170
- )
1171
- where = f"line {start}" if start == end else f"lines {start}–{end}"
1172
- href = open_at(editor, span.file, min(span.lit, default=start))
1173
- link = (
1174
- f' <a class="ced" href="{_esc(href)}" title="Open it in {_esc(editor_name(editor))}">open ↗</a>'
1175
- if href
1176
- else ""
1177
- )
1181
+ rows, above = [], ""
1182
+ for k in range(start, end + 1):
1183
+ mark = ""
1184
+ if history:
1185
+ commit = history.lines.get(k)
1186
+ said = commit.sha if commit else "u" if k in history.lines else "" # the commit, to say it once a run
1187
+ mark = blame_mark(history, k, keys, bool(said) and said == above)
1188
+ above = said
1189
+ rows.append(
1190
+ f'<span class="sl{" lit" if k in span.lit else ""}">{mark}<span class="sn">{k}</span>'
1191
+ f'<span class="sc">{lines[k - 1]}</span></span>'
1192
+ )
1193
+ places.append((span.file, start, end, more, open_at(editor, span.file, min(span.lit, default=start)), rows))
1194
+ if not places:
1195
+ return ""
1196
+
1197
+ def opens(href: str) -> str:
1198
+ return f'<a class="ced" href="{_esc(href)}">Open in {_esc(editor_name(editor))}</a>' if href else ""
1199
+
1200
+ shown = []
1201
+ for file, start, end, more, href, rows in places:
1202
+ if len(places) > 1: # one place is in the panel's head
1203
+ where = f"line {start}" if start == end else f"lines {start}–{end}"
1204
+ shown.append(f'<div class="csw">{_esc(file)} <span>{where}</span>{opens(href)}</div>')
1178
1205
  shown.append(
1179
- f'<div class="csw">{_esc(span.file)} <span>{where}</span>{link}</div>'
1180
- f'<pre class="csc">{rows}</pre>'
1206
+ f'<pre class="csc{" blame" if history else ""}">{"".join(rows)}</pre>'
1181
1207
  + (f'<div class="csm">+{_plural(more, "more line")}</div>' if more > 0 else "")
1182
1208
  )
1183
- if not shown:
1184
- return ""
1185
1209
  rest = len(n.code) - _G_PANEL_SPANS
1186
1210
  if rest > 0:
1187
1211
  shown.append(f'<div class="csm">+{_plural(rest, "more place")}: the tables below list them all</div>')
1188
- said = "".join( # not the calls this graph leaves out: a function's panel is the same in every graph it's in
1189
- f'<p class="csd">{_prose(line)}</p>'
1190
- for line in (n.tip or "").split("\n")
1191
- if line and not (n.more and line.startswith("It calls ") and "the graph leaves out" in line)
1192
- )
1193
- if lit_any:
1194
- said += (
1195
- '<p class="csd csl">The lit lines are where it calls, reads or sends what its arrows show.</p>'
1196
- if n.kind in ("handler", "function", "start")
1197
- else '<p class="csd csl">The lit lines are where the code uses it.</p>'
1212
+ mono = n.kind in ("handler", "function")
1213
+ said = []
1214
+ for line in (n.tip or "").split("\n"):
1215
+ if not line or (n.more and line.startswith("It calls ") and "the graph leaves out" in line):
1216
+ continue # not the calls this graph leaves out: a function's panel is the same in every graph it's in
1217
+ if mono and not said: # 'name(args) in file, 3 lines: its doc': its def is the code's first line
1218
+ head = re.fullmatch(r".+? in .+?, \d+ lines?(?:: (.*))?", line)
1219
+ if head:
1220
+ line = head.group(1) or ""
1221
+ if line:
1222
+ said.append(f'<p class="csd">{_prose(line)}</p>')
1223
+ about = [_esc(_G_PANEL_KINDS.get(n.kind, n.kind))] + ([_esc(n.sub)] if n.sub else [])
1224
+ if len(places) == 1 and places[0][4]:
1225
+ about.append(opens(places[0][4]))
1226
+ key = panel or _g_panel_id(n)
1227
+ opens_bar = bar = gear = ""
1228
+ if history: # its settings: a bar under its head that the gear opens, the checkbox before it so CSS can
1229
+ opens_bar = f'<input type="checkbox" class="cso" id="{_esc(key)}-set" aria-label="Settings">'
1230
+ gear = f'<label class="csx csgr" for="{_esc(key)}-set" title="Settings">{_G_GEAR}</label>'
1231
+ bar = (
1232
+ '<div class="csob"><label><input type="checkbox" class="csg" checked> '
1233
+ "Who changed each line, from git</label></div>"
1198
1234
  )
1199
- said += history_html(n.history)
1200
- mono = " mono" if n.kind in ("handler", "function") else ""
1201
1235
  return (
1202
- f'<aside class="cside" id="{_esc(panel or _g_panel_id(n))}" aria-label="{_esc("The code of " + n.label)}">'
1203
- f'<div class="csh"><div class="cst"><span class="csk">{_esc(_G_PANEL_KINDS.get(n.kind, n.kind))}</span>'
1204
- f'<b class="csn{mono}">{_esc(n.label)}</b>'
1205
- + (f'<span class="css">{_esc(n.sub)}</span>' if n.sub else "")
1206
- + '</div><a class="csx" href="#cn-closed" aria-label="Close" title="Close">×</a></div>'
1207
- f"{said}{''.join(shown)}</aside>"
1236
+ f'<aside class="cside" id="{_esc(key)}" aria-label="{_esc("The code of " + n.label)}">{opens_bar}'
1237
+ f'<div class="csh"><div class="cst"><b class="csn{" mono" if mono else ""}">{_esc(n.label)}</b>'
1238
+ f'<span class="css">{" · ".join(about)}</span></div>{gear}'
1239
+ f'<a class="csx" href="#cn-closed" aria-label="Close" title="Close">×</a></div>{bar}'
1240
+ f"{''.join(said)}{''.join(shown)}{blame_row(history, keys) if history else ''}</aside>"
1208
1241
  )
1209
1242
 
1210
1243
 
@@ -1446,6 +1479,27 @@ _G_SCRIPT = """<script>
1446
1479
  if (open && !open.contains(e.target) && !e.target.closest("a[href], label, input, .cgt")) close();
1447
1480
  });
1448
1481
  document.addEventListener("keydown", (e) => { if (e.key === "Escape") close(); });
1482
+ // A function's code: pointing at a line puts the commit it comes from in its panel's last row, in place of what git
1483
+ // says about all of it.
1484
+ // Its settings, one for every panel: turning git off in one turns it off in all, and the page remembers it.
1485
+ const blame = (on) => document.querySelectorAll(".csg").forEach((box) => { box.checked = on; });
1486
+ try { if (localStorage.getItem("code-node.blame") === "off") blame(false); } catch (e) {}
1487
+ document.addEventListener("change", (e) => {
1488
+ if (!e.target.matches(".csg")) return;
1489
+ blame(e.target.checked);
1490
+ try { localStorage.setItem("code-node.blame", e.target.checked ? "on" : "off"); } catch (e) {}
1491
+ });
1492
+ let blamed = "";
1493
+ document.addEventListener("mouseover", (e) => {
1494
+ const mark = e.target.closest(".cside .sl")?.querySelector(".sb[data-k]");
1495
+ const panel = mark && mark.closest(".cside"), key = mark ? mark.dataset.k : "";
1496
+ const row = (panel || document).querySelector(".csf");
1497
+ if (key + (panel && panel.id) === blamed) return;
1498
+ document.querySelectorAll(".csf p").forEach((p) => {
1499
+ p.hidden = p.closest(".csf") === row && key ? p.dataset.k !== key : p.hasAttribute("data-k");
1500
+ });
1501
+ blamed = key + (panel && panel.id);
1502
+ });
1449
1503
  // Moving about a graph as on a canvas: drag it to pan, Ctrl or ⌘ with the wheel (or a trackpad's pinch) to zoom
1450
1504
  // around the pointer, and + - 0 when it has focus. A zoom of its own unchecks the bar's buttons, so the plain rule
1451
1505
  // (the SVG's width times --z) draws it; picking a button again takes it back. Its box keeps one size, and Fit
@@ -31,7 +31,7 @@ FAVICON_LINK = f'<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,
31
31
  _ROOT = "cnd" # the class around each page's HTML: its stylesheet's rules are scoped to it
32
32
 
33
33
  _EXTRA_CSS = """\
34
- .cnd{--mono:"Cascadia Mono","Cascadia Code",ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace}
34
+ .cnd{--mono:"Cascadia Code","Cascadia Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace}
35
35
  .cnd code,.cnd table.t td.tree{font-family:var(--mono)}
36
36
  .cnd .wire{display:grid;grid-template-columns:minmax(0,1fr) 26px minmax(0,.9fr) 26px minmax(0,1fr);align-items:center;margin:6px 0 10px;max-width:980px}
37
37
  .cnd .wire .wc{display:flex;flex-direction:column;gap:6px;min-width:0}
@@ -280,25 +280,64 @@ _EXTRA_CSS = """\
280
280
  .cnd .cg .ctt .ctw li{margin:4px 0 0;overflow-wrap:anywhere}
281
281
  .cnd .cside{position:fixed;top:0;right:0;bottom:0;z-index:30;box-sizing:border-box;width:min(660px,94vw);overflow:auto;padding:0 20px 28px;background:var(--jp-layout-color0,var(--vscode-editor-background,Canvas));color:CanvasText;border-left:1px solid rgba(127,127,127,.3);box-shadow:-14px 0 36px rgba(15,23,42,.14);transform:translateX(104%);visibility:hidden;transition:transform .22s ease,visibility 0s linear .22s}
282
282
  .cnd .cside:target{transform:none;visibility:visible;transition:transform .22s ease}
283
- .cnd .cside .csh{display:flex;align-items:flex-start;gap:12px;position:sticky;top:0;z-index:1;margin:0 -20px 12px;padding:16px 20px 12px;background:var(--jp-layout-color0,var(--vscode-editor-background,Canvas));border-bottom:1px solid rgba(127,127,127,.2)}
283
+ .cnd .cside .csh{display:flex;align-items:flex-start;gap:12px;position:sticky;top:0;z-index:1;margin:0 -20px 12px;padding:18px 18px 16px 20px;background:var(--jp-layout-color0,var(--vscode-editor-background,Canvas));border-bottom:1px solid rgba(127,127,127,.2)}
284
284
  .cnd .cside .cst{flex:1;min-width:0}
285
- .cnd .cside .csk{display:block;font-size:11px;font-weight:650;letter-spacing:.08em;text-transform:uppercase;opacity:.55}
286
- .cnd .cside .csn{display:block;font-size:17px;font-weight:650;overflow-wrap:anywhere;margin:2px 0 1px}
287
- .cnd .cside .csn.mono{font:650 16px var(--mono)}
288
- .cnd .cside .css{font-size:12.5px;opacity:.65;overflow-wrap:anywhere}
289
- .cnd .cside a.csx{flex:none;display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:8px;font-size:22px;line-height:1;color:inherit;text-decoration:none;opacity:.6}
290
- .cnd .cside a.csx:hover,.cnd .cside a.csx:focus-visible{opacity:1;background:rgba(127,127,127,.14)}
285
+ .cnd .cside .csn{display:block;font-size:17px;font-weight:650;overflow-wrap:anywhere;margin:0 0 3px}
286
+ .cnd .cside .csn.mono{font:600 17px var(--mono)}
287
+ .cnd .cside .css{display:block;font-size:12.5px;overflow-wrap:anywhere;color:color-mix(in srgb,currentColor 65%,transparent)}
288
+ .cnd .cside .csx{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin:-4px -4px 0 0;border-radius:7px;font-size:20px;line-height:1;color:inherit;text-decoration:none;opacity:.5}
289
+ .cnd .cside .csx:hover,.cnd .cside .csx:focus-visible{opacity:1;background:rgba(127,127,127,.14)}
290
+ .cnd .cside .cso{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
291
+ .cnd .cside label.csgr{cursor:pointer;margin-right:-8px}
292
+ .cnd .cside .cso:focus-visible+.csh label.csgr{opacity:1;outline:2px solid light-dark(#2563eb,#6ea8fe)}
293
+ .cnd .cside .cso:checked+.csh label.csgr{opacity:1;background:rgba(127,127,127,.14)}
294
+ .cnd .cside .csob{display:none;align-items:center;gap:18px;flex-wrap:wrap;margin:-12px -20px 12px;padding:9px 20px;border-bottom:1px solid rgba(127,127,127,.2);background:rgba(127,127,127,.05);font-size:12.5px}
295
+ .cnd .cside .cso:checked~.csob{display:flex}
296
+ .cnd .cside .csob label{display:inline-flex;align-items:center;gap:7px;cursor:pointer}
297
+ .cnd .cside .csob input{margin:0;accent-color:light-dark(#2563eb,#6ea8fe)}
298
+ .cnd .cside:has(.csg:not(:checked)) :is(.sb,.csf){display:none}
299
+ .cnd .cside:has(.csg:not(:checked)) .blame .sn{padding-left:20px}
300
+ .cnd .cside:has(.csg:not(:checked)) .blame .sl:hover>*{background:none}
301
+ .cnd .cside:has(.csg:not(:checked)) .blame .sl.lit>:is(.sn,.sc){background:rgba(245,158,11,.14)}
291
302
  .cnd .cside .csd{font-size:13px;line-height:1.5;margin:0 0 6px;opacity:.82;overflow-wrap:anywhere}
292
- .cnd .cside .csl{font-size:12px;opacity:.6}
293
303
  .cnd .cside .csw{font:12px var(--mono);margin:16px 0 5px;overflow-wrap:anywhere}
294
304
  .cnd .cside .csw span{opacity:.55}
295
- .cnd .cside .ced{margin-left:6px;font-family:inherit;color:light-dark(#2563eb,#60a5fa);text-decoration:none}
305
+ .cnd .cside .csw .ced{margin-left:8px;font-family:inherit}
306
+ .cnd .cside .ced{color:light-dark(#2563eb,#6ea8fe);text-decoration:none}
296
307
  .cnd .cside .ced:hover{text-decoration:underline}
297
- .cnd .cside pre.csc{margin:0;padding:8px 0;border:1px solid rgba(127,127,127,.22);border-radius:8px;overflow:auto;background:rgba(127,127,127,.04);font:12.5px/1.6 var(--mono)}
298
- .cnd .cside .sl{display:block;width:max-content;min-width:100%;box-sizing:border-box;padding-right:14px;white-space:pre}
299
- .cnd .cside .sl.lit{background:rgba(245,158,11,.16);box-shadow:inset 3px 0 #f59e0b}
300
- .cnd .cside .sn{display:inline-block;width:3.2em;padding-right:14px;text-align:right;opacity:.42;user-select:none;-webkit-user-select:none}
308
+ .cnd .cside pre.csc{margin:12px -20px 0;padding:6px 0 10px;border:0;border-block:1px solid rgba(127,127,127,.2);border-radius:0;max-height:none;overflow:auto;font:12.5px/1.7 var(--mono)}
309
+ .cnd .cside :is(.csh,.csob)+pre.csc{margin-top:-12px;border-top:0}
310
+ .cnd .cside .sl{display:flex;width:max-content;min-width:100%}
311
+ .cnd .cside .sn{flex:none;width:2.4em;padding:0 16px 0 20px;text-align:right;opacity:.42;user-select:none;-webkit-user-select:none}
312
+ .cnd .cside .blame .sn{width:2.4em;padding-left:0}
313
+ .cnd .cside .sc{flex:1;padding-right:20px;white-space:pre}
314
+ .cnd .cside .sl.lit>:is(.sn,.sc){background:rgba(245,158,11,.14)}
315
+ .cnd .cside .sb{flex:none;display:flex;align-items:center;gap:10px;width:112px;box-sizing:border-box;padding-left:20px;font:11.5px -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;white-space:nowrap;user-select:none;-webkit-user-select:none}
316
+ .cnd .cside .sb span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;opacity:.5}
317
+ .cnd .cside .sb::after{content:"";flex:none;width:2px;align-self:stretch;margin-right:2px;background:var(--age)}
318
+ .cnd .cside .sb:not(.same)::after{margin-top:3px}
319
+ .cnd .cside .sl:first-child .sb::after{margin-top:0}
320
+ .cnd .cside .sb.same span{visibility:hidden}
321
+ .cnd .cside .sb.a0{--age:color-mix(in srgb,light-dark(#3b6fe0,#6ea8fe) 18%,transparent)}
322
+ .cnd .cside .sb.a1{--age:color-mix(in srgb,light-dark(#3b6fe0,#6ea8fe) 45%,transparent)}
323
+ .cnd .cside .sb.a2{--age:color-mix(in srgb,light-dark(#3b6fe0,#6ea8fe) 72%,transparent)}
324
+ .cnd .cside .sb.a3{--age:light-dark(#3b6fe0,#6ea8fe)}
325
+ .cnd .cside .sb.au{--age:light-dark(#c2680c,#f2b441)}
326
+ .cnd .cside .sb.au span{color:light-dark(#c2680c,#f2b441);opacity:1}
327
+ .cnd .cside .blame .sl:hover>*{background:rgba(127,127,127,.09)}
328
+ .cnd .cside .blame .sl.lit:hover>:is(.sn,.sc){background:rgba(245,158,11,.22)}
329
+ .cnd .cside .blame .sl:hover .sb span{visibility:visible;opacity:.9}
301
330
  .cnd .cside .csm{font-size:12px;font-style:italic;opacity:.6;margin:6px 2px 0}
331
+ .cnd .cside .csf{position:sticky;bottom:-28px;z-index:1;margin:0 -20px -28px;padding:11px 20px;background:var(--jp-layout-color0,var(--vscode-editor-background,Canvas));font-size:12.5px}
332
+ .cnd .cside .csf p{margin:0}
333
+ .cnd .cside .csf p:not([hidden]){display:flex;align-items:baseline;gap:12px}
334
+ .cnd .cside .csf p>span:first-child{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:color-mix(in srgb,currentColor 70%,transparent)}
335
+ .cnd .cside .csf b{font-weight:600}
336
+ .cnd .cside .csf .csfh{flex:none;color:light-dark(#2563eb,#6ea8fe);text-decoration:none}
337
+ .cnd .cside .csf a.csfh:hover{text-decoration:underline}
338
+ .cnd .cside .csf span.csfh{color:inherit;opacity:.6}
339
+ .cnd .cside .csf code{font:12px var(--mono);background:none;padding:0}
340
+ .cnd .cside .cnhu{color:light-dark(#b45309,#fbbf24)}
302
341
  @media (prefers-reduced-motion:reduce){.cnd .cside,.cnd .cside:target{transition:none}}
303
342
  .cnd .tabs{position:relative;margin:0 0 8px}
304
343
  .cnd .tbar{position:sticky;top:0;z-index:20;display:flex;flex-wrap:wrap;align-items:flex-end;gap:2px;padding-top:6px;margin:0 0 18px;border-bottom:1px solid rgba(127,127,127,.22);background:var(--jp-layout-color0,var(--vscode-editor-background,Canvas))}
@@ -383,8 +383,12 @@ def test_each_function_says_what_git_says_about_it(tmp_path: Path) -> None:
383
383
  assert main([str(root), "--no-open", "-o", str(out)]) == 0
384
384
  page = out.read_text()
385
385
  assert "by Ben in" in page and f'href="{load.last.url}"' in page and "2 lines changed since the last commit" in page
386
+ # the code's panel says beside each line who changed it, and in its last row what git says about all of it
387
+ assert '<pre class="csc blame">' in page and ">Ben · " in page and '<span class="sb au" data-k="u"' in page
388
+ assert "commits by 2 people" in page and 'class="csg" checked' in page
389
+ assert handler.lines and None in handler.lines.values() and load.lines[min(load.lines)] is not None
386
390
  assert main([str(root), "--no-open", "--no-git", "-o", str(out)]) == 0
387
- assert "Last changed" not in out.read_text()
391
+ assert "Last changed" not in out.read_text() and 'class="csg"' not in out.read_text()
388
392
  # a folder not in git, or whose own config would have git run a filter, has no history
389
393
  assert read_history(write(tmp_path / "plain", {"app.py": HANDLER}), cmap.functions[:1]) == 0
390
394
  subprocess.run(["git", "-C", str(root), "config", "filter.x.clean", "touch ran"], check=True)
@@ -1,42 +0,0 @@
1
- """
2
- What git says about a function, as HTML: who changed it last and when, with a link to the commit when the repository
3
- is on GitHub or GitLab, how many commits and people its lines come from, and its lines not committed yet. The same
4
- in the graph's panel, its hover card and the walk's card.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- from datetime import datetime, timezone
10
-
11
- from ..kit.text import _esc
12
-
13
- from ..analysis.model import History
14
-
15
-
16
- def history_html(history: History | None, short: bool = False) -> str:
17
- """
18
- The function's history as a few lines, or '' without one. short: the last change alone, for a hover card.
19
- """
20
- if history is None or not (history.last or history.uncommitted):
21
- return ""
22
- said = []
23
- last = history.last
24
- if last:
25
- day = datetime.fromtimestamp(last.time, timezone.utc).strftime("%Y-%m-%d")
26
- sha = f"<code>{_esc(last.short)}</code>"
27
- if last.url and not short:
28
- sha = f'<a href="{_esc(last.url)}" target="_blank" rel="noopener" title="The commit">{sha} ↗</a>'
29
- said.append(
30
- f'Last changed <b title="{day}">{_esc(history.ago(last.time))}</b> by {_esc(last.author)} in {sha}'
31
- + (f": <q>{_esc(last.subject)}</q>" if last.subject else "")
32
- )
33
- if history.commits > 1 and not short:
34
- people = f"{len(history.authors)} people" if len(history.authors) > 1 else _esc(history.authors[0])
35
- said.append(
36
- f"Its lines come from {history.commits} commits by {people}, the oldest "
37
- f"{_esc(history.ago(history.first))}"
38
- )
39
- if history.uncommitted:
40
- changed = f"{history.uncommitted} line{'' if history.uncommitted == 1 else 's'} changed since the last commit"
41
- said.append(f'<span class="cnhu">{changed if last else "Not committed yet"}</span>')
42
- return '<div class="cnh">' + "".join(f"<p>{line}</p>" for line in said) + "</div>"
File without changes