layouts-cvm 0.1.0__py3-none-any.whl

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 (77) hide show
  1. layouts_cvm/__init__.py +415 -0
  2. layouts_cvm/_internal/__init__.py +1 -0
  3. layouts_cvm/_internal/config/__init__.py +1 -0
  4. layouts_cvm/_internal/config/contracts/__init__.py +27 -0
  5. layouts_cvm/_internal/config/contracts/example_source.py +25 -0
  6. layouts_cvm/_internal/config/ports/__init__.py +7 -0
  7. layouts_cvm/_internal/config/ports/example_port.py +63 -0
  8. layouts_cvm/_internal/config/ports/submission_writer.py +59 -0
  9. layouts_cvm/_internal/utils/__init__.py +1 -0
  10. layouts_cvm/_internal/utils/atomic_write.py +95 -0
  11. layouts_cvm/_internal/utils/br_identifiers.py +288 -0
  12. layouts_cvm/_internal/utils/daily_cache.py +248 -0
  13. layouts_cvm/_internal/utils/dtypes.py +319 -0
  14. layouts_cvm/_internal/utils/http_downloader.py +249 -0
  15. layouts_cvm/_internal/utils/inr_mensal_fields.py +208 -0
  16. layouts_cvm/_internal/utils/logs.py +366 -0
  17. layouts_cvm/_internal/utils/logs_emitter.py +60 -0
  18. layouts_cvm/_internal/utils/provenance.py +183 -0
  19. layouts_cvm/_internal/utils/raw_workspace.py +73 -0
  20. layouts_cvm/_internal/utils/regime_adapter.py +76 -0
  21. layouts_cvm/_internal/utils/regime_registry.py +202 -0
  22. layouts_cvm/_internal/utils/regime_window.py +150 -0
  23. layouts_cvm/_internal/utils/retry/__init__.py +23 -0
  24. layouts_cvm/_internal/utils/retry/_schedule.py +77 -0
  25. layouts_cvm/_internal/utils/retry/backoff.py +212 -0
  26. layouts_cvm/_internal/utils/retry/log_emitter.py +55 -0
  27. layouts_cvm/_internal/utils/retry/policy.py +105 -0
  28. layouts_cvm/_internal/utils/sidecar_metadata.py +154 -0
  29. layouts_cvm/_internal/utils/spec_gap_registry.py +98 -0
  30. layouts_cvm/_internal/utils/tabular_reader.py +810 -0
  31. layouts_cvm/_internal/utils/text.py +79 -0
  32. layouts_cvm/_internal/utils/typing/__init__.py +30 -0
  33. layouts_cvm/_internal/utils/typing/abc_type_checker.py +16 -0
  34. layouts_cvm/_internal/utils/typing/decorators.py +34 -0
  35. layouts_cvm/_internal/utils/typing/policy.py +93 -0
  36. layouts_cvm/_internal/utils/typing/protocol_type_checker.py +20 -0
  37. layouts_cvm/_internal/utils/typing/type_checker.py +87 -0
  38. layouts_cvm/_internal/utils/typing/validate.py +75 -0
  39. layouts_cvm/_internal/utils/xml_fields.py +342 -0
  40. layouts_cvm/_internal/utils/xml_reader.py +497 -0
  41. layouts_cvm/_internal/utils/xml_writer.py +171 -0
  42. layouts_cvm/_internal/utils/zip_extractor.py +318 -0
  43. layouts_cvm/cadastro_ativos.py +80 -0
  44. layouts_cvm/cda.py +207 -0
  45. layouts_cvm/extrato.py +69 -0
  46. layouts_cvm/inf_fundo_157.py +81 -0
  47. layouts_cvm/inf_mensal_fidc.py +211 -0
  48. layouts_cvm/informe_diario.py +231 -0
  49. layouts_cvm/informe_diario_v3.py +42 -0
  50. layouts_cvm/informe_portabilidade.py +174 -0
  51. layouts_cvm/informe_quadrimestral.py +64 -0
  52. layouts_cvm/informe_trimestral_v1.py +54 -0
  53. layouts_cvm/inr_mensal.py +139 -0
  54. layouts_cvm/lamina.py +61 -0
  55. layouts_cvm/main.py +12 -0
  56. layouts_cvm/perfil_mensal.py +667 -0
  57. layouts_cvm/perfil_mensal_original.py +187 -0
  58. layouts_cvm/schemas/__init__.py +550 -0
  59. layouts_cvm/schemas/_fields.py +156 -0
  60. layouts_cvm/schemas/cadastro_ativos.py +288 -0
  61. layouts_cvm/schemas/cda.py +1357 -0
  62. layouts_cvm/schemas/extrato.py +758 -0
  63. layouts_cvm/schemas/inf_fundo_157.py +79 -0
  64. layouts_cvm/schemas/inf_mensal_fidc.py +1430 -0
  65. layouts_cvm/schemas/informe_diario.py +212 -0
  66. layouts_cvm/schemas/informe_diario_v3.py +73 -0
  67. layouts_cvm/schemas/informe_portabilidade.py +204 -0
  68. layouts_cvm/schemas/informe_quadrimestral.py +234 -0
  69. layouts_cvm/schemas/informe_trimestral_v1.py +123 -0
  70. layouts_cvm/schemas/inr_mensal.py +210 -0
  71. layouts_cvm/schemas/lamina.py +784 -0
  72. layouts_cvm/schemas/perfil_mensal.py +350 -0
  73. layouts_cvm/schemas/perfil_mensal_original.py +211 -0
  74. layouts_cvm-0.1.0.dist-info/METADATA +196 -0
  75. layouts_cvm-0.1.0.dist-info/RECORD +77 -0
  76. layouts_cvm-0.1.0.dist-info/WHEEL +4 -0
  77. layouts_cvm-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,415 @@
1
+ """layouts-cvm — CVM submission (envio) layouts.
2
+
3
+ Submission section — build and serialise files to *send* to CVM (envio).
4
+
5
+ Every "envio" solution lives here: it takes validated schema models (or a filled
6
+ spreadsheet) and produces a CVM-compliant file ready to submit. The reading/parsing
7
+ counterpart lives in the ``ingestion`` section.
8
+
9
+ The shared XML-standard models are re-exported here so callers construct documents
10
+ and serialise them through a single import:
11
+
12
+ from layouts_cvm import PerfilMensal, PerfilMensalDocument
13
+ """
14
+
15
+ from importlib.metadata import PackageNotFoundError, version
16
+
17
+ from layouts_cvm.cadastro_ativos import (
18
+ CadastroAtivos,
19
+ )
20
+ from layouts_cvm.cda import (
21
+ Cda,
22
+ )
23
+ from layouts_cvm.extrato import Extrato
24
+ from layouts_cvm.inf_fundo_157 import InfFundo157
25
+ from layouts_cvm.inf_mensal_fidc import (
26
+ InfMensalFidc,
27
+ )
28
+ from layouts_cvm.informe_diario import (
29
+ InformeDiario,
30
+ )
31
+ from layouts_cvm.informe_diario_v3 import InformeDiarioV3
32
+ from layouts_cvm.informe_portabilidade import InformePortabilidade
33
+ from layouts_cvm.informe_quadrimestral import InformeQuadrimestral
34
+ from layouts_cvm.informe_trimestral_v1 import InformeTrimestralV1
35
+ from layouts_cvm.inr_mensal import InrMensal
36
+ from layouts_cvm.lamina import Lamina
37
+ from layouts_cvm.main import main
38
+ from layouts_cvm.perfil_mensal import (
39
+ PerfilMensal,
40
+ )
41
+ from layouts_cvm.perfil_mensal_original import PerfilMensalOriginal
42
+ from layouts_cvm.schemas.cadastro_ativos import (
43
+ CadastroAtivosDocument,
44
+ CadastroAtivosHeader,
45
+ CadastroAtivosRow,
46
+ )
47
+ from layouts_cvm.schemas.cda import (
48
+ CdaAplic,
49
+ CdaAtivoExt,
50
+ CdaBankEmitter,
51
+ CdaCotas,
52
+ CdaDemaisCodif,
53
+ CdaDemaisNCodif,
54
+ CdaDepPrazoTitIf,
55
+ CdaDocument,
56
+ CdaEmitter,
57
+ CdaHeader,
58
+ CdaInform,
59
+ CdaInvExt,
60
+ CdaNegoc,
61
+ CdaPosFim,
62
+ CdaPosFixIndex,
63
+ CdaRiskRating,
64
+ CdaSwap,
65
+ CdaSwapAplic,
66
+ CdaSwapPosFim,
67
+ CdaTitAgroCredPriv,
68
+ CdaTitPub,
69
+ )
70
+ from layouts_cvm.schemas.extrato import (
71
+ ExtratoDocument,
72
+ ExtratoHeader,
73
+ ExtratoRow,
74
+ )
75
+ from layouts_cvm.schemas.inf_fundo_157 import (
76
+ InfFundo157Cotista,
77
+ InfFundo157Document,
78
+ InfFundo157Header,
79
+ )
80
+ from layouts_cvm.schemas.inf_mensal_fidc import (
81
+ Alienacao,
82
+ Amort,
83
+ Amortizacao,
84
+ AplicAtivo,
85
+ CaptaResgaAmorti,
86
+ CaptMes,
87
+ CartSegmt,
88
+ Cedent,
89
+ CedentCredExiste,
90
+ ClasseCotistas,
91
+ CompmtDicred,
92
+ CotasMovimento,
93
+ CotistasPorTipo,
94
+ CredExiste,
95
+ DescSerieClasse,
96
+ Desemp,
97
+ DesempClasse,
98
+ DevSacado,
99
+ Dicred,
100
+ Garantia,
101
+ InfMensalFidcDocument,
102
+ InfMensalFidcHeader,
103
+ InfMensalFidcInform,
104
+ InformSacados,
105
+ Liquidez,
106
+ ListaCedent,
107
+ ListaCedentCredExiste,
108
+ ListaDevSacados,
109
+ MercDerivativo,
110
+ NegocDicredMes,
111
+ NumCotistas,
112
+ NumCotistasDesc,
113
+ OutrasInform,
114
+ OutrosAtivos,
115
+ Passiv,
116
+ PassivPosicoes,
117
+ PassivValores,
118
+ Patrliq,
119
+ QtVl,
120
+ RentClasse,
121
+ RentMes,
122
+ ResgMes,
123
+ ResgSolic,
124
+ SegmtComerc,
125
+ SegmtFact,
126
+ SegmtFinanc,
127
+ SegmtServ,
128
+ SegmtSetorPublic,
129
+ SerieClasse,
130
+ Taxa,
131
+ TaxaAquis,
132
+ TaxaAtivRf,
133
+ TaxaCdb,
134
+ TaxaNegocDicredMes,
135
+ TaxaSemAquis,
136
+ TaxaTitpubFed,
137
+ TaxaValorMobili,
138
+ ValoresMob,
139
+ )
140
+ from layouts_cvm.schemas.informe_diario import (
141
+ InformeDiarioDocument,
142
+ InformeDiarioHeader,
143
+ InformeDiarioInform,
144
+ SignificantShareholder,
145
+ )
146
+ from layouts_cvm.schemas.informe_diario_v3 import (
147
+ InformeDiarioV3Document,
148
+ InformeDiarioV3Header,
149
+ InformeDiarioV3Inform,
150
+ )
151
+ from layouts_cvm.schemas.informe_portabilidade import (
152
+ InformePortabilidadeDocument,
153
+ PortabilidadeHeader,
154
+ PortabilidadeInform,
155
+ )
156
+ from layouts_cvm.schemas.informe_quadrimestral import (
157
+ ClasseCotas,
158
+ CotistCategory,
159
+ InformeQuadrimestralDocument,
160
+ InformeQuadrimestralHeader,
161
+ InformeQuadrimestralInform,
162
+ ListaClasseCotas,
163
+ ListaCotstCateg,
164
+ )
165
+ from layouts_cvm.schemas.informe_trimestral_v1 import (
166
+ InformeTrimestralV1Document,
167
+ InformeTrimestralV1Header,
168
+ InformeTrimestralV1Inform,
169
+ )
170
+ from layouts_cvm.schemas.inr_mensal import (
171
+ Aplicacoes,
172
+ EntradaSaida,
173
+ InrMensalDocument,
174
+ InrMensalHeader,
175
+ MovRecursos,
176
+ Participante,
177
+ RecursosEnvMesmoInvest,
178
+ RecursosEnvOutroInvest,
179
+ RecursosMesmoInvest,
180
+ RecursosOutroInvest,
181
+ RecursosRecebMesmoInvest,
182
+ RecursosRecebOutroInvest,
183
+ TransfRecursos,
184
+ )
185
+ from layouts_cvm.schemas.lamina import (
186
+ LaminaAntesInic2,
187
+ LaminaCarencia,
188
+ LaminaCompCarteira,
189
+ LaminaConversao,
190
+ LaminaDespesas,
191
+ LaminaDocument,
192
+ LaminaHeader,
193
+ LaminaInform,
194
+ LaminaInformExemploComp,
195
+ LaminaInforTabRentAnual,
196
+ LaminaListaCompCarteira,
197
+ LaminaListaTabRentAnual,
198
+ LaminaListaTabRentMensal,
199
+ LaminaTabRentAnual,
200
+ LaminaTabRentMensal,
201
+ LaminaTabSimulDespesas,
202
+ LaminaTabVarDesemp,
203
+ LaminaTaxaAdministracao,
204
+ LaminaTaxaEntrada,
205
+ LaminaTaxaSaida,
206
+ )
207
+ from layouts_cvm.schemas.perfil_mensal import (
208
+ ClientCount,
209
+ DocumentHeader,
210
+ NominalRiskBlock,
211
+ NominalRiskFactor,
212
+ OtcOperation,
213
+ PatrimonyDistribution,
214
+ PerfilMensalDocument,
215
+ PerfilMensalRow,
216
+ PerformanceFeeDetails,
217
+ PrimitiveRiskFactor,
218
+ PrivateCreditIssuer,
219
+ VarOutros,
220
+ VarPercValCota,
221
+ )
222
+ from layouts_cvm.schemas.perfil_mensal_original import (
223
+ PerfilMensalOriginalClientCount,
224
+ PerfilMensalOriginalDocument,
225
+ PerfilMensalOriginalHeader,
226
+ PerfilMensalOriginalPatrimonyDistribution,
227
+ PerfilMensalOriginalRow,
228
+ PerfilMensalOriginalTradingDays,
229
+ )
230
+
231
+
232
+ try:
233
+ __version__ = version("layouts-cvm")
234
+ except PackageNotFoundError: # pragma: no cover - source tree without an installed dist
235
+ __version__ = "0.0.0"
236
+
237
+
238
+ __all__ = [
239
+ "__version__",
240
+ "main",
241
+ "Alienacao",
242
+ "Amort",
243
+ "Amortizacao",
244
+ "AplicAtivo",
245
+ "Aplicacoes",
246
+ "CadastroAtivos",
247
+ "CadastroAtivosDocument",
248
+ "CadastroAtivosHeader",
249
+ "CadastroAtivosRow",
250
+ "CaptMes",
251
+ "CaptaResgaAmorti",
252
+ "CartSegmt",
253
+ "Cda",
254
+ "CdaAplic",
255
+ "CdaAtivoExt",
256
+ "CdaBankEmitter",
257
+ "CdaCotas",
258
+ "CdaDemaisCodif",
259
+ "CdaDemaisNCodif",
260
+ "CdaDepPrazoTitIf",
261
+ "CdaDocument",
262
+ "CdaEmitter",
263
+ "CdaHeader",
264
+ "CdaInform",
265
+ "CdaInvExt",
266
+ "CdaNegoc",
267
+ "CdaPosFim",
268
+ "CdaPosFixIndex",
269
+ "CdaRiskRating",
270
+ "CdaSwap",
271
+ "CdaSwapAplic",
272
+ "CdaSwapPosFim",
273
+ "CdaTitAgroCredPriv",
274
+ "CdaTitPub",
275
+ "Cedent",
276
+ "CedentCredExiste",
277
+ "ClasseCotas",
278
+ "ClasseCotistas",
279
+ "ClientCount",
280
+ "CompmtDicred",
281
+ "CotasMovimento",
282
+ "CotistCategory",
283
+ "CotistasPorTipo",
284
+ "CredExiste",
285
+ "DescSerieClasse",
286
+ "Desemp",
287
+ "DesempClasse",
288
+ "DevSacado",
289
+ "Dicred",
290
+ "DocumentHeader",
291
+ "EntradaSaida",
292
+ "Extrato",
293
+ "ExtratoDocument",
294
+ "ExtratoHeader",
295
+ "ExtratoRow",
296
+ "Garantia",
297
+ "InfFundo157",
298
+ "InfFundo157Cotista",
299
+ "InfFundo157Document",
300
+ "InfFundo157Header",
301
+ "InfMensalFidc",
302
+ "InfMensalFidcDocument",
303
+ "InfMensalFidcHeader",
304
+ "InfMensalFidcInform",
305
+ "InformSacados",
306
+ "InformeDiario",
307
+ "InformeDiarioDocument",
308
+ "InformeDiarioHeader",
309
+ "InformeDiarioInform",
310
+ "InformeDiarioV3",
311
+ "InformeDiarioV3Document",
312
+ "InformeDiarioV3Header",
313
+ "InformeDiarioV3Inform",
314
+ "InformePortabilidade",
315
+ "InformePortabilidadeDocument",
316
+ "InformeQuadrimestral",
317
+ "InformeQuadrimestralDocument",
318
+ "InformeQuadrimestralHeader",
319
+ "InformeQuadrimestralInform",
320
+ "InformeTrimestralV1",
321
+ "InformeTrimestralV1Document",
322
+ "InformeTrimestralV1Header",
323
+ "InformeTrimestralV1Inform",
324
+ "InrMensal",
325
+ "InrMensalDocument",
326
+ "InrMensalHeader",
327
+ "Lamina",
328
+ "LaminaAntesInic2",
329
+ "LaminaCarencia",
330
+ "LaminaCompCarteira",
331
+ "LaminaConversao",
332
+ "LaminaDespesas",
333
+ "LaminaDocument",
334
+ "LaminaHeader",
335
+ "LaminaInforTabRentAnual",
336
+ "LaminaInform",
337
+ "LaminaInformExemploComp",
338
+ "LaminaListaCompCarteira",
339
+ "LaminaListaTabRentAnual",
340
+ "LaminaListaTabRentMensal",
341
+ "LaminaTabRentAnual",
342
+ "LaminaTabRentMensal",
343
+ "LaminaTabSimulDespesas",
344
+ "LaminaTabVarDesemp",
345
+ "LaminaTaxaAdministracao",
346
+ "LaminaTaxaEntrada",
347
+ "LaminaTaxaSaida",
348
+ "Liquidez",
349
+ "ListaCedent",
350
+ "ListaCedentCredExiste",
351
+ "ListaClasseCotas",
352
+ "ListaCotstCateg",
353
+ "ListaDevSacados",
354
+ "MercDerivativo",
355
+ "MovRecursos",
356
+ "NegocDicredMes",
357
+ "NominalRiskBlock",
358
+ "NominalRiskFactor",
359
+ "NumCotistas",
360
+ "NumCotistasDesc",
361
+ "OtcOperation",
362
+ "OutrasInform",
363
+ "OutrosAtivos",
364
+ "Participante",
365
+ "Passiv",
366
+ "PassivPosicoes",
367
+ "PassivValores",
368
+ "PatrimonyDistribution",
369
+ "Patrliq",
370
+ "PerfilMensal",
371
+ "PerfilMensalDocument",
372
+ "PerfilMensalOriginal",
373
+ "PerfilMensalOriginalClientCount",
374
+ "PerfilMensalOriginalDocument",
375
+ "PerfilMensalOriginalHeader",
376
+ "PerfilMensalOriginalPatrimonyDistribution",
377
+ "PerfilMensalOriginalRow",
378
+ "PerfilMensalOriginalTradingDays",
379
+ "PerfilMensalRow",
380
+ "PerformanceFeeDetails",
381
+ "PortabilidadeHeader",
382
+ "PortabilidadeInform",
383
+ "PrimitiveRiskFactor",
384
+ "PrivateCreditIssuer",
385
+ "QtVl",
386
+ "RecursosEnvMesmoInvest",
387
+ "RecursosEnvOutroInvest",
388
+ "RecursosMesmoInvest",
389
+ "RecursosOutroInvest",
390
+ "RecursosRecebMesmoInvest",
391
+ "RecursosRecebOutroInvest",
392
+ "RentClasse",
393
+ "RentMes",
394
+ "ResgMes",
395
+ "ResgSolic",
396
+ "SegmtComerc",
397
+ "SegmtFact",
398
+ "SegmtFinanc",
399
+ "SegmtServ",
400
+ "SegmtSetorPublic",
401
+ "SerieClasse",
402
+ "SignificantShareholder",
403
+ "Taxa",
404
+ "TaxaAquis",
405
+ "TaxaAtivRf",
406
+ "TaxaCdb",
407
+ "TaxaNegocDicredMes",
408
+ "TaxaSemAquis",
409
+ "TaxaTitpubFed",
410
+ "TaxaValorMobili",
411
+ "TransfRecursos",
412
+ "ValoresMob",
413
+ "VarOutros",
414
+ "VarPercValCota",
415
+ ]
@@ -0,0 +1 @@
1
+ """Private internals of the layouts_cvm package (not a public API)."""
@@ -0,0 +1 @@
1
+ """Private configuration internals (data contracts) of layouts_cvm."""
@@ -0,0 +1,27 @@
1
+ """Data contracts for the project's input files (config layer).
2
+
3
+ A contract is **declarative configuration** of an input's expected shape — which columns a
4
+ source must carry, which must hold valid CNPJs — *not* data access, so contracts live in
5
+ ``config`` beside the other declarative config (``inputs.yaml``, ``connection_db``), imported
6
+ by the model loaders and the controller boundary.
7
+
8
+ Convention: **one file per source** under this package (``cadastro.py``, ``orders.py``, …),
9
+ each defining a single ``FileContract`` instance; this aggregator re-exports them (plus the
10
+ machinery from ``utils.tabular_reader``) so callers import from one place:
11
+ ``from config.contracts import EXAMPLE_SOURCE, find_file_problems``.
12
+
13
+ ``EXAMPLE_SOURCE`` is a reference instance — copy ``example_source.py`` per real source and
14
+ delete the example once your own contracts exist.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from layouts_cvm._internal.config.contracts.example_source import EXAMPLE_SOURCE
20
+ from layouts_cvm._internal.utils.tabular_reader import (
21
+ ContractError,
22
+ FileContract,
23
+ find_file_problems,
24
+ )
25
+
26
+
27
+ __all__ = ["EXAMPLE_SOURCE", "ContractError", "FileContract", "find_file_problems"]
@@ -0,0 +1,25 @@
1
+ """Reference data contract — copy this per real input source, then delete the example.
2
+
3
+ Declares the columns the file must carry (``tuple_required``) and the columns that must hold
4
+ at least one valid CNPJ (``tuple_cnpj_cols``). The model/controller passes this instance to
5
+ ``utils.tabular_reader.read_table`` / ``read_query``, which raises ``ContractError`` on any
6
+ violation before types are applied.
7
+
8
+ ⚠️ Before copying this file for a real source, **confirm which document is the contract and
9
+ get the owner's sign-off** — writing a reader against the wrong spec produces a file that
10
+ parses cleanly and carries the wrong columns, which no test here can catch. The reasoning is
11
+ in BlueprintX's ``templates/python-common/src/config/contracts/CLAUDE.md`` (upstream; not
12
+ copied into generated projects — blueprintx#325).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from layouts_cvm._internal.utils.tabular_reader import FileContract
18
+
19
+
20
+ EXAMPLE_SOURCE = FileContract(
21
+ "Example Source",
22
+ "example_source",
23
+ ("code", "amount"),
24
+ (),
25
+ )
@@ -0,0 +1,7 @@
1
+ """Ports - the behavioural boundary interfaces of the library (hexagonal, private)."""
2
+
3
+ from layouts_cvm._internal.config.ports.example_port import ExamplePort
4
+ from layouts_cvm._internal.config.ports.submission_writer import SubmissionWriter, TDoc
5
+
6
+
7
+ __all__ = ["ExamplePort", "SubmissionWriter", "TDoc"]
@@ -0,0 +1,63 @@
1
+ """Reference behavioural port — copy this per real macro-section operation, then delete it.
2
+
3
+ In hexagonal *ports-and-adapters* terms this ABC is a **port**: it pins the single operation
4
+ that a family of concrete classes (the *adapters*) all implement, so callers can treat any
5
+ adapter polymorphically and every new variant conforms to the same shape. It is private
6
+ (``_internal``): consumers import the concrete adapters, never this port.
7
+
8
+ The port is generic over ``_T`` (the value it operates on) so an adapter narrows the type
9
+ through the type parameter — e.g. ``class CsvHandler(ExamplePort[pd.DataFrame])`` — rather than
10
+ by re-annotating the override, keeping the concrete signature Liskov-compatible. ``_T`` is
11
+ unbounded here; bind it (``TypeVar("_T", bound=BaseModel)``) when every adapter shares a base.
12
+
13
+ The leading underscore is deliberate: an adapter parameterises the port
14
+ (``ExamplePort[pd.DataFrame]``) and never imports the type variable by name, so ``_T`` is an
15
+ implementation detail of this module — the same convention the typeshed stubs use. Naming it
16
+ ``T`` made it a public member this package's ``__all__`` did not export, which
17
+ ``bin/check_all_exports.py`` correctly rejects (blueprintx#246).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from abc import abstractmethod
23
+ from typing import Generic, TypeVar
24
+
25
+ from layouts_cvm._internal.utils.typing import ABCTypeCheckerMeta
26
+
27
+
28
+ _T = TypeVar("_T")
29
+
30
+
31
+ class ExamplePort(Generic[_T], metaclass=ABCTypeCheckerMeta):
32
+ """Contract for one behavioural operation shared across a macro-section's adapters.
33
+
34
+ ``ABCTypeCheckerMeta`` gives both abstract-method enforcement (a partial adapter fails at
35
+ instantiation) and runtime type checking of every call. Concrete adapters inherit the port
36
+ and its metaclass — do **not** redeclare ``metaclass`` on a subclass; Python inherits it.
37
+
38
+ Methods
39
+ -------
40
+ handle(item)
41
+ Perform the port's single operation on ``item`` and return the result.
42
+ """
43
+
44
+ @abstractmethod
45
+ def handle(self, item: _T) -> _T:
46
+ """Perform the operation on ``item`` and return the result.
47
+
48
+ Parameters
49
+ ----------
50
+ item : _T
51
+ The value the concrete adapter operates on.
52
+
53
+ Returns
54
+ -------
55
+ _T
56
+ The operation's result.
57
+
58
+ Raises
59
+ ------
60
+ NotImplementedError
61
+ Always — a concrete adapter must override this method.
62
+ """
63
+ raise NotImplementedError
@@ -0,0 +1,59 @@
1
+ """Submission writer port — the shared contract every *envio* writer (adapter) implements.
2
+
3
+ Every ``submission`` solution turns a validated schema document into a CVM-compliant
4
+ file to send to CVM. This ABC is the **port** (hexagonal ports-and-adapters) pinning that
5
+ single operation — :meth:`export` — so callers can treat any writer polymorphically and
6
+ every new standard implements the same shape. It is private (``_internal``): consumers
7
+ import the concrete writers (the adapters), never this port.
8
+
9
+ The document type varies per standard, so the port is generic over ``TDoc`` (bound
10
+ to :class:`pydantic.BaseModel`): a concrete writer conforms by parameterising it, e.g.
11
+ ``class InformeDiario(SubmissionWriter[InformeDiarioDocument])``. This narrows the
12
+ document type through the type parameter rather than by re-annotating an override, so
13
+ the concrete signature stays Liskov-compatible.
14
+
15
+ CVM's submission standards are all XML today, so :meth:`export` produces XML; the generic
16
+ name leaves room for a future non-XML export without changing the contract.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from abc import abstractmethod
22
+ from typing import Generic, TypeVar
23
+
24
+ from pydantic import BaseModel
25
+
26
+ from layouts_cvm._internal.utils.typing import ABCTypeCheckerMeta
27
+
28
+
29
+ TDoc = TypeVar("TDoc", bound=BaseModel)
30
+
31
+
32
+ class SubmissionWriter(Generic[TDoc], metaclass=ABCTypeCheckerMeta):
33
+ """Contract for a submission (envio) writer: validated document → CVM file.
34
+
35
+ Methods
36
+ -------
37
+ export(doc, output_path)
38
+ Serialise a validated document to a CVM-compliant file string, optionally
39
+ writing it to ``output_path``.
40
+ """
41
+
42
+ @abstractmethod
43
+ def export(self, doc: TDoc, output_path: str | None = None) -> str | None:
44
+ """Serialise ``doc`` to a CVM-compliant file (string, or written to disk).
45
+
46
+ Parameters
47
+ ----------
48
+ doc : TDoc
49
+ Fully validated document model for the standard.
50
+ output_path : str, optional
51
+ Destination file path. When given, the file is written and ``None`` is
52
+ returned; otherwise the serialised string is returned. By default ``None``.
53
+
54
+ Returns
55
+ -------
56
+ str or None
57
+ The serialised content when ``output_path`` is ``None``, else ``None``.
58
+ """
59
+ raise NotImplementedError
@@ -0,0 +1 @@
1
+ """Shared, project-agnostic utility helpers (copied into every Python skeleton)."""
@@ -0,0 +1,95 @@
1
+ """Atomic file output for the submission writers.
2
+
3
+ A writer's destination must always be either the previous file or the complete new one,
4
+ never a partial write left behind by an I/O fault.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import contextlib
10
+ import os
11
+ from pathlib import Path
12
+ import secrets
13
+
14
+ from layouts_cvm._internal.utils.typing import type_checker
15
+
16
+
17
+ @type_checker
18
+ def atomic_write_bytes(path: str | Path, payload: bytes) -> None:
19
+ """Write ``payload`` to ``path`` atomically.
20
+
21
+ The bytes go to a temp file in the destination's own directory (so ``os.replace`` stays
22
+ on one filesystem), are flushed and fsynced, then replace the target. If the write,
23
+ fsync or replace fails, the temp file is removed and a pre-existing target is left
24
+ untouched. The directory fsync runs after the replace: if it fails, the new file is
25
+ already in place and ``OSError`` is still raised.
26
+
27
+ The caller therefore needs write permission on the destination's parent directory, not
28
+ only on an existing target file. A pre-existing target keeps its permission bits; a new
29
+ file gets the mode a plain ``open()`` would give (``0o666`` minus the umask).
30
+
31
+ Parameters
32
+ ----------
33
+ path : str | Path
34
+ Destination file path.
35
+ payload : bytes
36
+ Exact bytes to write.
37
+
38
+ Raises
39
+ ------
40
+ OSError
41
+ If the write, fsync or replace fails, or if the directory fsync fails after the
42
+ replace (the target then already holds the new payload).
43
+ """
44
+ path_dest = Path(path)
45
+ path_tmp = path_dest.with_name(f".{path_dest.name}.{secrets.token_hex(8)}.tmp")
46
+ flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY | getattr(os, "O_BINARY", 0)
47
+ fd = os.open(path_tmp, flags, 0o666)
48
+ try:
49
+ _apply_dest_mode(path_tmp, path_dest)
50
+ with os.fdopen(fd, "wb") as file_tmp:
51
+ file_tmp.write(payload)
52
+ file_tmp.flush()
53
+ os.fsync(file_tmp.fileno())
54
+ os.replace(path_tmp, path_dest)
55
+ except BaseException:
56
+ with contextlib.suppress(FileNotFoundError):
57
+ os.unlink(path_tmp)
58
+ raise
59
+ _fsync_dir(path_dest.parent)
60
+
61
+
62
+ @type_checker
63
+ def _apply_dest_mode(path_tmp: Path, path_dest: Path) -> None:
64
+ """Give the still-empty temp file the existing destination's permission bits.
65
+
66
+ Runs before any payload byte is written, so the payload is never readable under a wider
67
+ mode than the final file. A missing destination leaves the ``0o666``-minus-umask default.
68
+
69
+ Parameters
70
+ ----------
71
+ path_tmp : Path
72
+ Temp file, opened but not yet written.
73
+ path_dest : Path
74
+ Final destination, which may not exist yet.
75
+ """
76
+ with contextlib.suppress(FileNotFoundError):
77
+ os.chmod(path_tmp, os.stat(path_dest).st_mode & 0o7777)
78
+
79
+
80
+ @type_checker
81
+ def _fsync_dir(path_dir: Path) -> None:
82
+ """Fsync the directory so the rename survives a crash (POSIX; no-op elsewhere).
83
+
84
+ Parameters
85
+ ----------
86
+ path_dir : Path
87
+ Directory holding the renamed file.
88
+ """
89
+ if os.name != "posix":
90
+ return
91
+ fd = os.open(path_dir, os.O_RDONLY)
92
+ try:
93
+ os.fsync(fd)
94
+ finally:
95
+ os.close(fd)