atlassian-cli 0.10.3__tar.gz → 0.10.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.
- {atlassian_cli-0.10.3/src/atlassian_cli.egg-info → atlassian_cli-0.10.4}/PKG-INFO +1 -1
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/pyproject.toml +1 -1
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli/adf.py +169 -15
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4/src/atlassian_cli.egg-info}/PKG-INFO +1 -1
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/tests/test_adf.py +121 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/LICENSE +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/README.md +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/setup.cfg +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli/__init__.py +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli/config.py +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli/confluence.py +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli/hints.py +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli/http.py +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli/jira.py +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli/jira_assets.py +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli/jira_issues.py +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli/output.py +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli/update_check.py +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli.egg-info/SOURCES.txt +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli.egg-info/dependency_links.txt +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli.egg-info/entry_points.txt +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli.egg-info/requires.txt +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli.egg-info/top_level.txt +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/tests/test_config.py +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/tests/test_confluence.py +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/tests/test_http.py +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/tests/test_jira_assets.py +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/tests/test_jira_issues.py +0 -0
- {atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/tests/test_output.py +0 -0
|
@@ -17,6 +17,7 @@ Provides:
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
import re
|
|
20
|
+
import uuid
|
|
20
21
|
|
|
21
22
|
from atlas_doc_parser.api import NodeDoc
|
|
22
23
|
|
|
@@ -297,22 +298,63 @@ def _to_list_item(item):
|
|
|
297
298
|
return {'type': 'listItem', 'content': [para(str(item))]}
|
|
298
299
|
|
|
299
300
|
|
|
300
|
-
def table(header_cells, rows):
|
|
301
|
-
"""Create a table. header_cells/rows: lists of strings or inline-node lists.
|
|
302
|
-
|
|
301
|
+
def table(header_cells, rows, *, colwidths=None, localid=None):
|
|
302
|
+
"""Create a table. header_cells/rows: lists of strings or inline-node lists.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
header_cells: Column headings (strings or inline-node lists).
|
|
306
|
+
rows: Body rows, each a list of cell values.
|
|
307
|
+
colwidths: Optional list of column widths (int/float). If omitted,
|
|
308
|
+
divides 900 equally.
|
|
309
|
+
localid: Optional table-level UUID. If omitted, generated.
|
|
310
|
+
"""
|
|
311
|
+
n_cols = len(header_cells)
|
|
312
|
+
if colwidths is None:
|
|
313
|
+
colwidths = [round(900.0 / n_cols, 1)] * n_cols
|
|
314
|
+
|
|
315
|
+
def _uuid():
|
|
316
|
+
return str(uuid.uuid4())
|
|
317
|
+
|
|
318
|
+
def cell(val, col_idx, is_header=False, row_localid=None):
|
|
303
319
|
ct = 'tableHeader' if is_header else 'tableCell'
|
|
320
|
+
colwidth = colwidths[col_idx] if col_idx < len(colwidths) else colwidths[-1]
|
|
321
|
+
attrs = {'colspan': 1, 'colwidth': colwidth, 'rowspan': 1}
|
|
304
322
|
if isinstance(val, str):
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
323
|
+
content_nodes = [para(val)]
|
|
324
|
+
elif isinstance(val, list):
|
|
325
|
+
content_nodes = [para(*val)]
|
|
326
|
+
else:
|
|
327
|
+
content_nodes = [para(str(val))]
|
|
328
|
+
node = {
|
|
329
|
+
'type': ct,
|
|
330
|
+
'attrs': attrs,
|
|
331
|
+
'content': content_nodes,
|
|
332
|
+
'localId': _uuid(),
|
|
333
|
+
}
|
|
334
|
+
return node
|
|
335
|
+
|
|
336
|
+
tbl_localid = localid or _uuid()
|
|
337
|
+
|
|
338
|
+
content = [
|
|
339
|
+
{
|
|
340
|
+
'type': 'tableRow',
|
|
341
|
+
'content': [cell(c, ci, is_header=True) for ci, c in enumerate(header_cells)],
|
|
342
|
+
'localId': _uuid(),
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
for ri, row in enumerate(rows):
|
|
346
|
+
row_localid = _uuid()
|
|
347
|
+
cells = []
|
|
348
|
+
for ci, c in enumerate(row):
|
|
349
|
+
cells.append(cell(c, ci, is_header=False))
|
|
350
|
+
content.append({
|
|
351
|
+
'type': 'tableRow',
|
|
352
|
+
'content': cells,
|
|
353
|
+
'localId': row_localid,
|
|
354
|
+
})
|
|
313
355
|
return {
|
|
314
356
|
'type': 'table',
|
|
315
|
-
'attrs': {'isNumberColumnEnabled': False, 'layout': 'default', 'localId':
|
|
357
|
+
'attrs': {'isNumberColumnEnabled': False, 'layout': 'default', 'localId': tbl_localid},
|
|
316
358
|
'content': content,
|
|
317
359
|
}
|
|
318
360
|
|
|
@@ -342,8 +384,8 @@ def md_to_adf(markdown):
|
|
|
342
384
|
"""Convert a markdown string to a list of ADF nodes.
|
|
343
385
|
|
|
344
386
|
Supports: headings, paragraphs, bullet/ordered lists, bold, italic,
|
|
345
|
-
bold+italic, inline code, links, horizontal rules, code blocks, blockquotes
|
|
346
|
-
|
|
387
|
+
bold+italic, inline code, links, horizontal rules, code blocks, blockquotes,
|
|
388
|
+
tables (newline-separated and inline-flattened forms).
|
|
347
389
|
"""
|
|
348
390
|
lines = markdown.split('\n')
|
|
349
391
|
nodes = []
|
|
@@ -414,17 +456,129 @@ def md_to_adf(markdown):
|
|
|
414
456
|
]})
|
|
415
457
|
continue
|
|
416
458
|
|
|
459
|
+
# Table (newline-separated Form A)
|
|
460
|
+
if line.startswith('|'):
|
|
461
|
+
tbl_result = _try_parse_table(lines, i)
|
|
462
|
+
if tbl_result:
|
|
463
|
+
nodes.append(tbl_result[0])
|
|
464
|
+
i = tbl_result[1]
|
|
465
|
+
continue
|
|
466
|
+
|
|
417
467
|
# Paragraph — consecutive non-blank, non-block lines
|
|
418
468
|
para_lines = []
|
|
419
469
|
while i < len(lines) and lines[i].strip() and not _is_block_start(lines[i]):
|
|
420
470
|
para_lines.append(lines[i])
|
|
421
471
|
i += 1
|
|
422
472
|
if para_lines:
|
|
423
|
-
|
|
473
|
+
joined = ' '.join(para_lines)
|
|
474
|
+
# Table (inline-flattened Form B) — detect inside paragraph
|
|
475
|
+
if _looks_like_inline_table(joined):
|
|
476
|
+
tbl_node = _parse_inline_table(joined)
|
|
477
|
+
if tbl_node:
|
|
478
|
+
nodes.append(tbl_node)
|
|
479
|
+
continue
|
|
480
|
+
nodes.append({'type': 'paragraph', 'content': _parse_inline(joined)})
|
|
424
481
|
|
|
425
482
|
return nodes
|
|
426
483
|
|
|
427
484
|
|
|
485
|
+
_SEP_RE = re.compile(r'^:?-+:?$')
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _is_separator_row(cell_texts):
|
|
489
|
+
"""Return True if every non-empty cell matches the separator pattern."""
|
|
490
|
+
return all(_SEP_RE.match(c.strip()) for c in cell_texts if c.strip())
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _split_row(line):
|
|
494
|
+
"""Split a ``|...|...|`` markdown table row into cell strings."""
|
|
495
|
+
parts = line.strip().split('|')
|
|
496
|
+
# first and last are empty (leading/trailing pipe)
|
|
497
|
+
if parts and parts[0] == '':
|
|
498
|
+
parts = parts[1:]
|
|
499
|
+
if parts and parts[-1] == '':
|
|
500
|
+
parts = parts[:-1]
|
|
501
|
+
return [p.strip() for p in parts]
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _looks_like_inline_table(text_str):
|
|
505
|
+
"""Check if a single-line paragraph looks like a flattened markdown table."""
|
|
506
|
+
# Must contain a separator pattern: |---| somewhere
|
|
507
|
+
if not re.search(r'\|\s*:?-{3,}:?\s*\|', text_str):
|
|
508
|
+
return False
|
|
509
|
+
# Must have at least 2 pipes (3 cells minimum after split)
|
|
510
|
+
return text_str.count('|') >= 4
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _parse_inline_table(text_str):
|
|
514
|
+
"""Parse an inline-flattened markdown table into an ADF table node.
|
|
515
|
+
|
|
516
|
+
Row boundaries are ``|`` followed by whitespace then ``|`` (closing pipe
|
|
517
|
+
of one row immediately followed by opening pipe of the next).
|
|
518
|
+
"""
|
|
519
|
+
raw_rows = re.split(r'\|\s+(?=\|)', text_str)
|
|
520
|
+
return _build_table_from_rows(raw_rows)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _try_parse_table(lines, start):
|
|
524
|
+
"""Try to parse a newline-separated markdown table starting at *start*.
|
|
525
|
+
|
|
526
|
+
Returns ``(table_node, new_index)`` or ``None`` if not a valid table.
|
|
527
|
+
"""
|
|
528
|
+
i = start
|
|
529
|
+
raw_rows = []
|
|
530
|
+
while i < len(lines) and lines[i].strip().startswith('|'):
|
|
531
|
+
raw_rows.append(lines[i].strip())
|
|
532
|
+
i += 1
|
|
533
|
+
|
|
534
|
+
if len(raw_rows) < 2:
|
|
535
|
+
return None
|
|
536
|
+
# The second row must be a separator
|
|
537
|
+
cells = _split_row(raw_rows[1])
|
|
538
|
+
if not _is_separator_row(cells):
|
|
539
|
+
return None
|
|
540
|
+
|
|
541
|
+
return _build_table_from_rows(raw_rows), i
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _build_table_from_rows(raw_rows):
|
|
545
|
+
"""Build an ADF table node from a list of ``|...|`` row strings.
|
|
546
|
+
|
|
547
|
+
First row = header, second row = separator (dropped), remainder = body.
|
|
548
|
+
"""
|
|
549
|
+
if len(raw_rows) < 2:
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
header_cells = _split_row(raw_rows[0])
|
|
553
|
+
n_cols = len(header_cells)
|
|
554
|
+
|
|
555
|
+
body_cells = []
|
|
556
|
+
for ri in range(2, len(raw_rows)):
|
|
557
|
+
row = _split_row(raw_rows[ri])
|
|
558
|
+
if not any(r.strip() for r in row):
|
|
559
|
+
continue
|
|
560
|
+
if len(row) < n_cols:
|
|
561
|
+
row.extend([''] * (n_cols - len(row)))
|
|
562
|
+
elif len(row) > n_cols:
|
|
563
|
+
row = row[:n_cols]
|
|
564
|
+
body_cells.append(row)
|
|
565
|
+
|
|
566
|
+
# Parse inline content for header cells, add strong mark
|
|
567
|
+
parsed_header = []
|
|
568
|
+
for h in header_cells:
|
|
569
|
+
inlines = _parse_inline(h)
|
|
570
|
+
for node in inlines:
|
|
571
|
+
if node.get('type') == 'text':
|
|
572
|
+
node.setdefault('marks', []).insert(0, {'type': 'strong'})
|
|
573
|
+
parsed_header.append(inlines)
|
|
574
|
+
|
|
575
|
+
parsed_body = []
|
|
576
|
+
for row in body_cells:
|
|
577
|
+
parsed_body.append([_parse_inline(c) for c in row])
|
|
578
|
+
|
|
579
|
+
return table(parsed_header, parsed_body)
|
|
580
|
+
|
|
581
|
+
|
|
428
582
|
def _is_block_start(line):
|
|
429
583
|
if re.match(r'^#{1,6}\s', line):
|
|
430
584
|
return True
|
|
@@ -492,3 +492,124 @@ More text.
|
|
|
492
492
|
assert 'Test' in md_out
|
|
493
493
|
assert 'world' in md_out
|
|
494
494
|
assert 'item one' in md_out
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# ---------------------------------------------------------------------------
|
|
498
|
+
# md_to_adf — table support
|
|
499
|
+
# ---------------------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
class TestMdToAdfTables:
|
|
502
|
+
def test_md_table_standard(self):
|
|
503
|
+
"""Newline-separated markdown table produces valid ADF table."""
|
|
504
|
+
md = "\n".join([
|
|
505
|
+
"| Header 1 | Header 2 | Header 3 |",
|
|
506
|
+
"| --- | --- | --- |",
|
|
507
|
+
"| a | b | c |",
|
|
508
|
+
"| d | e | f |",
|
|
509
|
+
])
|
|
510
|
+
nodes = md_to_adf(md)
|
|
511
|
+
assert len(nodes) == 1
|
|
512
|
+
t = nodes[0]
|
|
513
|
+
assert t['type'] == 'table'
|
|
514
|
+
assert 'localId' in t['attrs']
|
|
515
|
+
assert t['attrs']['localId'] != ''
|
|
516
|
+
rows = t['content']
|
|
517
|
+
assert len(rows) == 3 # header + 2 data, separator dropped
|
|
518
|
+
assert rows[0]['type'] == 'tableRow'
|
|
519
|
+
assert 'localId' in rows[0]
|
|
520
|
+
assert rows[0]['localId'] != ''
|
|
521
|
+
hdr = rows[0]['content'][0]
|
|
522
|
+
assert hdr['type'] == 'tableHeader'
|
|
523
|
+
assert 'localId' in hdr
|
|
524
|
+
assert hdr['localId'] != ''
|
|
525
|
+
hdr_text = hdr['content'][0]['content'][0]
|
|
526
|
+
assert hdr_text['marks'][0]['type'] == 'strong'
|
|
527
|
+
body_cell = rows[1]['content'][0]
|
|
528
|
+
assert body_cell['type'] == 'tableCell'
|
|
529
|
+
assert body_cell['localId'] != ''
|
|
530
|
+
assert 'colwidth' in body_cell['attrs']
|
|
531
|
+
|
|
532
|
+
def test_md_table_inline_flattened(self):
|
|
533
|
+
"""Inline-flattened table (bug we hit) produces same ADF table."""
|
|
534
|
+
md = "| Header 1 | Header 2 | Header 3 | | --- | --- | --- | | a | b | c | | d | e | f |"
|
|
535
|
+
nodes = md_to_adf(md)
|
|
536
|
+
assert len(nodes) == 1
|
|
537
|
+
t = nodes[0]
|
|
538
|
+
assert t['type'] == 'table'
|
|
539
|
+
rows = t['content']
|
|
540
|
+
assert len(rows) == 3
|
|
541
|
+
assert rows[0]['content'][0]['type'] == 'tableHeader'
|
|
542
|
+
assert rows[1]['content'][0]['type'] == 'tableCell'
|
|
543
|
+
|
|
544
|
+
def test_md_table_inline_formatting(self):
|
|
545
|
+
"""Cells with bold, italic, code, links produce correct inline marks."""
|
|
546
|
+
md = "| A | B | | --- | --- | | **bold** | *italic* | | `code` | [link](https://example.com) |"
|
|
547
|
+
nodes = md_to_adf(md)
|
|
548
|
+
t = nodes[0]
|
|
549
|
+
cells = t['content'][1]['content']
|
|
550
|
+
c0 = cells[0]['content'][0]['content'][0]
|
|
551
|
+
c1 = cells[1]['content'][0]['content'][0]
|
|
552
|
+
c2 = t['content'][2]['content'][0]['content'][0]['content'][0]
|
|
553
|
+
c3 = t['content'][2]['content'][1]['content'][0]['content'][0]
|
|
554
|
+
assert c0['marks'][0]['type'] == 'strong'
|
|
555
|
+
assert c1['marks'][0]['type'] == 'em'
|
|
556
|
+
assert c2['marks'][0]['type'] == 'code'
|
|
557
|
+
assert c3['marks'][0]['type'] == 'link'
|
|
558
|
+
|
|
559
|
+
def test_md_table_alignment_separator(self):
|
|
560
|
+
"""Alignment markers in separator (:--, :-:, --:) are recognised and dropped."""
|
|
561
|
+
md = "\n".join([
|
|
562
|
+
"| L | C | R |",
|
|
563
|
+
"| :-- | :-: | --: |",
|
|
564
|
+
"| a | b | c |",
|
|
565
|
+
])
|
|
566
|
+
nodes = md_to_adf(md)
|
|
567
|
+
t = nodes[0]
|
|
568
|
+
assert len(t['content']) == 2 # header + 1 data, separator dropped
|
|
569
|
+
|
|
570
|
+
def test_md_table_ragged_rows(self):
|
|
571
|
+
"""Short rows pad with empty cells; long rows truncate."""
|
|
572
|
+
md = "\n".join([
|
|
573
|
+
"| A | B | C |",
|
|
574
|
+
"| --- | --- | --- |",
|
|
575
|
+
"| 1 | 2 |", # short
|
|
576
|
+
"| 4 | 5 | 6 | 7 |", # long
|
|
577
|
+
])
|
|
578
|
+
nodes = md_to_adf(md)
|
|
579
|
+
t = nodes[0]
|
|
580
|
+
r1 = t['content'][1]['content'] # short row
|
|
581
|
+
r2 = t['content'][2]['content'] # long row
|
|
582
|
+
assert len(r1) == 3
|
|
583
|
+
assert len(r2) == 3
|
|
584
|
+
|
|
585
|
+
def test_md_table_in_mixed_document(self):
|
|
586
|
+
"""Table sits between heading and bullet list, all separate nodes."""
|
|
587
|
+
md = "\n".join([
|
|
588
|
+
"## Before",
|
|
589
|
+
"",
|
|
590
|
+
"| A | B |",
|
|
591
|
+
"| --- | --- |",
|
|
592
|
+
"| 1 | 2 |",
|
|
593
|
+
"",
|
|
594
|
+
"- item one",
|
|
595
|
+
"- item two",
|
|
596
|
+
])
|
|
597
|
+
nodes = md_to_adf(md)
|
|
598
|
+
types = [n['type'] for n in nodes]
|
|
599
|
+
assert types == ['heading', 'table', 'bulletList']
|
|
600
|
+
|
|
601
|
+
def test_md_table_localids_present(self):
|
|
602
|
+
"""Every table/row/header/cell has a non-empty localId."""
|
|
603
|
+
md = "\n".join([
|
|
604
|
+
"| A | B |",
|
|
605
|
+
"| --- | --- |",
|
|
606
|
+
"| 1 | 2 |",
|
|
607
|
+
])
|
|
608
|
+
nodes = md_to_adf(md)
|
|
609
|
+
t = nodes[0]
|
|
610
|
+
assert t['attrs']['localId'] != ''
|
|
611
|
+
for row in t['content']:
|
|
612
|
+
assert row['localId'] != ''
|
|
613
|
+
for cell in row['content']:
|
|
614
|
+
assert cell['localId'] != ''
|
|
615
|
+
assert 'colwidth' in cell['attrs']
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{atlassian_cli-0.10.3 → atlassian_cli-0.10.4}/src/atlassian_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|